mojmajstor/app/_layout.tsx
echo 5102ed23c1 feat: Phase 1 - project setup, auth, and base UI
Set up Expo SDK 56 project with TypeScript and Expo Router (file-based).

Backend:
- Full Convex schema (users, accounts, sessions, ads, categories,
  reviews, chats, messages, posts) deployed to self-hosted instance
- Custom email/password auth with SHA-256 hashing via Convex actions
- Query/mutation scaffolds for all domain tables
- Convex environment variables configured on backend

App structure:
- Root layout with ConvexProvider, ThemeProvider, AuthContext
- (auth) group: login and register screens (Macedonian UI)
  with role selection (handyman/customer)
- (tabs) group: 5 tabs (Home, Explore, Posts, Chat, Profile)
  each with nested Stack layouts
- Placeholder detail screens for ad, post, chat, review flows
- 404 not-found screen in Macedonian

UI primitives (components/ui/):
- Button (4 variants), Input, Card, Loading, Avatar, Rating, Badge
- ThemeProvider with light/dark palette via React context

All user-facing strings in Macedonian. Code identifiers in English.
TypeScript passes with zero errors.
2026-05-29 18:18:58 +02:00

83 lines
2.6 KiB
TypeScript

import { Stack } from "expo-router/stack";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { useState, createContext, useContext, useEffect, useCallback } from "react";
import { ThemeProvider } from "../components/theme";
const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!);
type AuthState = {
token: string | null;
userId: string | null;
isAuthenticated: boolean;
login: (token: string, userId: string) => Promise<void>;
logout: () => Promise<void>;
};
const AuthContext = createContext<AuthState>({
token: null,
userId: null,
isAuthenticated: false,
login: async () => {},
logout: async () => {},
});
export function useAuth() {
return useContext(AuthContext);
}
const TOKEN_KEY = "mojmajstor_auth_token";
const USER_ID_KEY = "mojmajstor_auth_userId";
export default function RootLayout() {
const [token, setToken] = useState<string | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
Promise.all([
localStorage.getItem(TOKEN_KEY),
localStorage.getItem(USER_ID_KEY),
]).then(([t, u]) => {
if (t) setToken(t);
if (u) setUserId(u);
setLoaded(true);
});
}, []);
const login = useCallback(async (newToken: string, newUserId: string) => {
localStorage.setItem(TOKEN_KEY, newToken);
localStorage.setItem(USER_ID_KEY, newUserId);
setToken(newToken);
setUserId(newUserId);
}, []);
const logout = useCallback(async () => {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_ID_KEY);
setToken(null);
setUserId(null);
}, []);
if (!loaded) return null;
return (
<ConvexProvider client={convex}>
<ThemeProvider>
<AuthContext.Provider
value={{ token, userId, isAuthenticated: !!token, login, logout }}
>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(auth)" />
<Stack.Screen name="(tabs)" />
<Stack.Screen name="ad/[id]" options={{ presentation: "card" }} />
<Stack.Screen name="ad/create" options={{ presentation: "modal" }} />
<Stack.Screen name="post/[id]" options={{ presentation: "card" }} />
<Stack.Screen name="post/create" options={{ presentation: "modal" }} />
<Stack.Screen name="chat/[id]" options={{ presentation: "card" }} />
<Stack.Screen name="review/create" options={{ presentation: "modal" }} />
</Stack>
</AuthContext.Provider>
</ThemeProvider>
</ConvexProvider>
);
}