React + TypeScript + Vite web application for Muhammadiyah organization profile. Uses shadcn/ui components, Tailwind CSS v4, TanStack Query, Zustand, and React Router.
# Development server (port 5173)
npm run dev
# Production build
npm run build
# Lint with ESLint
npm run lint
# Preview production build
npm run preview- Target: ES2022, strict mode enabled
- All components must be typed; use explicit return types for hooks
- Path alias
@/maps to./src/ - Prefer
typeimports:import type { Foo } from '...' - No unused locals or parameters (compiler enforced)
- React imports
- Third-party libraries (grouped)
- Absolute imports (
@/components,@/lib, etc.) - Relative imports
- Type-only imports last
// Use function declarations for components
function ComponentName({ prop1, prop2 }: Props) {
return <div>...</div>
}
// Export at bottom
export { ComponentName }
// For components with variants, use cva from class-variance-authority
const componentVariants = cva("base-classes", {
variants: { variant: { default: "..." } },
defaultVariants: { variant: "default" }
})- Components: PascalCase (
Button.tsx,ArticleCard.tsx) - Hooks: camelCase with
useprefix (useAuth.ts,useArticles.ts) - Stores: camelCase with
Storesuffix (authStore.ts) - Types/Interfaces: PascalCase (
User,ArticleStatus) - Enum-like unions: PascalCase (
type ArticleStatus = 'DRAFT' | 'PUBLISHED') - API functions: Group in
api/index.ts, export as object
src/
components/
ui/ # shadcn/ui components (don't modify)
editor/ # Tiptap editor components
layout/ # Reusable layout components (Header, Footer, PublicLayout)
features/
{feature}/
api/ # API functions
components/# Feature-specific components
hooks/ # Feature-specific hooks
schemas/ # Zod validation schemas
stores/ # Zustand stores
hooks/ # Global hooks
lib/ # Utilities (cn function)
pages/ # Route components
public/ # Public-facing pages
profile/ # Organization profile pages (Visi-Misi, Sejarah, etc.)
admin/ # Admin dashboard pages
shared/
components/ # Shared components (ErrorBoundary)
lib/ # Shared utilities (api.ts)
types/ # Global TypeScript types
- Dark Mode: Uses
ThemeProviderwithlocalStoragepersistence - Theme Variables: Defined in
index.cssusingoklch - Color Tokens: Use semantic tokens instead of static colors:
bg-background,bg-card,bg-muted,bg-accenttext-foreground,text-muted-foreground,text-primaryborder,input,ring
- Utility classes with
cn()from@/shared/lib/utilsfor conditional classes - Follow shadcn patterns:
data-slotattributes, semantic color tokens
- Global state: Zustand with persistence middleware when needed
- Server state: TanStack Query (React Query)
- Form state: React Hook Form + Zod validation
- API errors: Throw in API layer, catch in hooks/components
- Use
sonnerfor toast notifications - Form validation errors: Display inline with field messages
- Global Errors: Wrapped with
ErrorBoundaryinmain.tsx. Custom fallback can be provided viafallbackprop.
- Standard: Use Skeleton components instead of spinners for a "premium" feel.
- Location: Feature-specific skeletons go in
features/{feature}/components/(e.g.,ArticleListSkeleton). - Implementation: Check
isLoadingfrom TanStack Query and render the skeleton.
// In feature/api/index.ts
import { get, post, patch, del } from '@/shared/lib/api';
export const featureApi = {
getAll: () => get<Item[]>('/endpoint'),
create: (data: CreateDto) => post<Item>('/endpoint', data),
update: (id: string, data: UpdateDto) => patch<Item>(`/endpoint/${id}`, data),
delete: (id: string) => del<void>(`/endpoint/${id}`),
};export const useItems = () => {
return useQuery({
queryKey: ['items'],
queryFn: featureApi.getAll,
staleTime: 5 * 60 * 1000,
});
};
export const useCreateItem = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: featureApi.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['items'] });
},
});
};- Public Layout: All public-facing pages must be wrapped with
PublicLayoutinApp.tsx. - Reusable Components: Layout-related components like
HeaderandFootermust reside insrc/components/layout/. - Admin Layout: Admin pages use
AdminLayoutfromfeatures/admin/components/. - Dynamic Data:
HeaderandFooterMUST NOT hardcode site identity, contact, or social media info. Always useuseSetting(key)from@/features/settings/hooks/useSettings.
// App.tsx routing pattern
<Route element={<PublicLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/artikel" element={<ArticlesPage />} />
<Route path="/profil/visi-misi" element={<VisionMissionPage />} />
<Route path="/profil/sejarah" element={<HistoryPage />} />
</Route>- Public data (site name, contact, social): use
useSetting(key)orusePublicSettings()— hits/api/v1/settings/public. - Admin data (all settings): use
useAdminSettings()— hits/api/v1/settings(requires ADMIN role). - Mutations: use
useUpdateBulkSettings()for saving multiple fields at once. - Cache invalidation: mutations automatically invalidate both admin and public query keys.
- Fallback: always provide a sensible fallback string when using
useSetting().
- Access: Only for
ADMINrole. Route:/admin/pengguna. - Toggling Status: Use
useToggleUserStatusmutation hook forisActivetoggle. - Form Handling: Use
UserFormwhich integrates withreact-hook-formandzod. - Cache: Admin user list uses the query key
['admin', 'users']. Mutations invalidate this key.
import { useSetting } from '@/features/settings/hooks/useSettings';
// In any component
const siteName = useSetting('site_name'); // string, '' if not set
const email = useSetting('contact_email');
const display = siteName || 'Organisasi Kami'; // always provide fallback- Schema Location: All Zod schemas must be placed in
features/{feature}/schemas/. - Validation: Use React Hook Form + @hookform/resolvers/zod.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { itemSchema, type ItemFormData } from '../schemas/itemSchema';
const form = useForm<ItemFormData>({
resolver: zodResolver(itemSchema),
defaultValues: { ... },
});- UI:
@radix-ui/*,lucide-react,class-variance-authority,clsx,tailwind-merge - Forms:
react-hook-form,@hookform/resolvers,zod - Data:
@tanstack/react-query,axios - State:
zustand - Editor:
@tiptap/react,@tiptap/starter-kit - Date:
date-fns
VITE_API_URL- Backend API base URL (defaults tohttp://localhost:3000/api/v1)- Access via
import.meta.env.VITE_*
- This is a client-side only React app (not RSC)
- Authentication uses JWT stored in localStorage + HttpOnly Refresh Token in cookies
- Proxy configured in
vite.config.tsfor local development - Uses React 19 with StrictMode
- All shadcn/ui components are in
@/components/ui- don't modify, extend via wrapper components
To solve CORS and Cookie issues without a custom domain, we use Vercel Rewrites. The frontend project handles the routing.
-
Backend Deployment:
- Deploy as a separate project.
- Use
vercel.jsonwith@vercel/nodebuilder. - Entry point:
api/index.ts(registersmodule-aliasandtsconfig-paths). package.jsonmust have"postinstall": "prisma generate".
-
Frontend Deployment:
- Deploy as a separate project.
- Use
vercel.jsonto rewrite/api/:path*to the Vercel backend URL. - Set SPA routing: rewrite all other paths to
/index.html.
Backend:
DATABASE_URL: Neon PostgreSQL connection string.JWT_SECRET: Secret key for token signing.FRONTEND_URL: URL of the deployed frontend (for CORS).CLOUDINARY_*: Credentials for image uploads.
Frontend:
- No
VITE_API_URLneeded in production if using rewrites (requests go to/api/v1).