Skip to main content

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

TechnologyVersionPurpose
React18.3.1UI library
Next.js14.2.35Framework & routing
Vite6.3.5Build tool
SWR2.2.5Data fetching
Tailwind CSS3.4.17Styling
TypeScript5.5.4Type 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

layout.tsx
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

page.tsx
'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:
Button.tsx
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

pnpm dev:frontend
Runs on http://localhost:5173 (Vite default)

Build for Production

pnpm build:frontend

Environment Variables

.env
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

1

Use custom components

Never install UI component libraries. Build custom components for full control.
2

Follow SWR patterns

Each SWR hook must be in its own function. Never return multiple hooks.
3

Use useFetch hook

Always use the custom useFetch hook for API calls.
4

Check color system

Review colors.scss and use new color variables, not deprecated ones.
5

Check existing components

Before creating a component, check components/ui for existing patterns.
6

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

Build docs developers (and LLMs) love