Overview
The Postiz frontend is built with React 18 , Vite , and Next.js 14 App Router . It emphasizes custom components, SWR for data fetching, and Tailwind CSS for styling.
Critical Rule : Never install frontend components from npm. Focus on writing native, custom components.
Technology Stack
Technology Version Purpose React 18.3.1 UI library Next.js 14.2.35 Framework & routing Vite 6.3.5 Build tool SWR 2.2.5 Data fetching Tailwind CSS 3.4.17 Styling TypeScript 5.5.4 Type safety
Project Structure
apps/frontend/src/
├── app/
│ ├── (app)/ # Protected app routes
│ │ ├── (site)/ # Main site routes
│ │ │ ├── analytics/
│ │ │ ├── media/
│ │ │ ├── launches/
│ │ │ ├── settings/
│ │ │ └── layout.tsx
│ │ └── layout.tsx
│ ├── auth/ # Authentication routes
│ ├── api/ # API routes (Next.js)
│ ├── colors.scss # Color variables
│ └── global.scss # Global styles
├── components/
│ ├── ui/ # UI components
│ ├── layout/ # Layout components
│ ├── calendar/ # Calendar components
│ ├── analytics/ # Analytics components
│ └── ... (feature components)
├── middleware.ts # Next.js middleware
└── instrumentation.ts # Instrumentation
Key Conventions
SWR Data Fetching
Critical : Each SWR hook must be in a separate function to comply with React hooks rules. Never use eslint-disable-next-line for hooks violations.
Valid Pattern:
// ✓ Correct - each hook is separate
const useCommunity = () => {
return useSWR < CommunitiesListResponse >( "communities" , getCommunities );
};
const useProviders = () => {
return useSWR < ProvidersListResponse >( "providers" , getProviders );
};
Invalid Pattern:
// ✗ Wrong - never return multiple hooks
const useCommunity = () => {
return {
communities : () => useSWR < CommunitiesListResponse >( "communities" , getCommunities ),
providers : () => useSWR < ProvidersListResponse >( "providers" , getProviders ),
};
};
Custom Fetch Hook
Always use the useFetch hook from helpers:
import { useFetch } from '@gitroom/helpers/utils/custom.fetch.tsx' ;
function MyComponent () {
const fetch = useFetch ();
const loadData = async () => {
const data = await fetch ( '/api/posts' );
return data ;
};
}
Component Organization
Many UI components live in /apps/frontend/src/components/ui. Check existing components before creating new ones to maintain design consistency.
Styling Guidelines
Tailwind Configuration
Before writing any component, review these files:
/apps/frontend/src/app/colors.scss - Color variables
/apps/frontend/src/app/global.scss - Global styles
/apps/frontend/tailwind.config.js - Tailwind config
All --color-custom* variables are deprecated. Do not use them. Use the new color system instead.
Color System
Use CSS custom properties defined in colors.scss:
// ✓ Correct - use new color variables
< div className = "bg-newBgColor text-newTextColor" >
< button className = "bg-btnPrimary text-btnText" >
Click me
</ button >
</ div >
// ✗ Wrong - deprecated colors
< div className = "bg-customColor1 text-customColor2" >
...
</ div >
Available Color Variables:
--color-primary
--color-secondary
--new-bgColor
--new-bgColorInner
--new-textColor
--new-btn-primary
--new-btn-simple
--new-btn-text
--new-border
--new-separator
Tailwind Classes
// Layout
< div className = "flex flex-col gap-4 p-6" >
// Responsive
< div className = "mobile:hidden tablet:flex" >
// Custom screens (from tailwind.config.js)
< div className = "custom:text-sm tablet:text-base" >
// Colors
< div className = "bg-newBgColor border-newBorder text-newTextColor" >
App Router Structure
Route Groups
Next.js 14 uses route groups with parentheses:
app/
├── (app)/ # Protected routes
│ └── (site)/ # Main app routes
│ ├── analytics/
│ ├── media/
│ └── settings/
├── auth/ # Public auth routes
└── api/ # API routes
Layout Pattern
import { ReactNode } from 'react' ;
import '../global.scss' ;
import LayoutContext from '@gitroom/frontend/components/layout/layout.context' ;
export default async function AppLayout ({ children } : { children : ReactNode }) {
return (
< html >
< body className = "dark text-primary !bg-primary" >
< LayoutContext >
{ children }
</ LayoutContext >
</ body >
</ html >
);
}
Page Component
'use client' ;
import { useFetch } from '@gitroom/helpers/utils/custom.fetch.tsx' ;
import useSWR from 'swr' ;
export default function AnalyticsPage () {
const fetch = useFetch ();
const { data , error , isLoading } = useSWR (
'analytics' ,
() => fetch ( '/api/analytics' )
);
if ( isLoading ) return < div > Loading ...</ div > ;
if ( error ) return < div > Error loading analytics </ div > ;
return (
< div className = "flex flex-col gap-6 p-6" >
< h1 className = "text-2xl font-bold" > Analytics </ h1 >
{ /* Component content */ }
</ div >
);
}
State Management
SWR for Server State
import useSWR from 'swr' ;
import { useFetch } from '@gitroom/helpers/utils/custom.fetch.tsx' ;
const usePosts = () => {
const fetch = useFetch ();
return useSWR ( 'posts' , async () => {
return await fetch ( '/api/posts' );
});
};
function PostsList () {
const { data : posts , error , mutate } = usePosts ();
const createPost = async ( postData ) => {
await fetch ( '/api/posts' , {
method: 'POST' ,
body: JSON . stringify ( postData ),
});
// Revalidate
mutate ();
};
}
Zustand for Client State
For local UI state, use Zustand:
import { create } from 'zustand' ;
interface AppState {
sidebarOpen : boolean ;
toggleSidebar : () => void ;
}
const useAppStore = create < AppState >(( set ) => ({
sidebarOpen: true ,
toggleSidebar : () => set (( state ) => ({ sidebarOpen: ! state . sidebarOpen })),
}));
function Sidebar () {
const { sidebarOpen , toggleSidebar } = useAppStore ();
return (
< aside className = {sidebarOpen ? 'block' : 'hidden' } >
{ /* Sidebar content */ }
</ aside >
);
}
Component Patterns
Client vs Server Components
// Server Component (default)
export default async function Page () {
const data = await fetchData (); // Can fetch directly
return < div >{data. title } </ div > ;
}
// Client Component (use 'use client')
'use client' ;
import { useState } from 'react' ;
export default function InteractiveComponent () {
const [ count , setCount ] = useState ( 0 );
return < button onClick ={() => setCount ( count + 1 )}>{ count } </ button > ;
}
Custom Components
Always build custom components instead of using external libraries:
interface ButtonProps {
children : React . ReactNode ;
onClick ?: () => void ;
variant ?: 'primary' | 'secondary' ;
disabled ?: boolean ;
}
export function Button ({
children ,
onClick ,
variant = 'primary' ,
disabled
} : ButtonProps ) {
return (
< button
onClick = { onClick }
disabled = { disabled }
className = { `
px-4 py-2 rounded-lg font-medium transition-colors
${ variant === 'primary' ? 'bg-btnPrimary text-btnText' : 'bg-btnSimple text-btnText' }
${ disabled ? 'opacity-50 cursor-not-allowed' : 'hover:opacity-90' }
` }
>
{ children }
</ button >
);
}
Development Workflow
Start Development Server
Runs on http://localhost:5173 (Vite default)
Build for Production
Environment Variables
NEXT_PUBLIC_BACKEND_URL = "http://localhost:3000"
FRONTEND_URL = "http://localhost:5173"
NEXT_PUBLIC_SENTRY_DSN = "..."
Variables prefixed with NEXT_PUBLIC_ are exposed to the browser.
Best Practices
Use custom components
Never install UI component libraries. Build custom components for full control.
Follow SWR patterns
Each SWR hook must be in its own function. Never return multiple hooks.
Use useFetch hook
Always use the custom useFetch hook for API calls.
Check color system
Review colors.scss and use new color variables, not deprecated ones.
Check existing components
Before creating a component, check components/ui for existing patterns.
Run linting from root
Linting can only run from the root directory.
Next Steps
Component Architecture Learn component patterns and structure
Routing Structure Understand Next.js App Router
Styling Guide Master Tailwind CSS styling