mojmajstor/app/(auth)/register.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

158 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from "react";
import {
View,
Text,
KeyboardAvoidingView,
Platform,
ScrollView,
Alert,
Pressable,
} from "react-native";
import { useRouter } from "expo-router";
import { useMutation, useAction } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Input } from "../../components/ui/input";
import { Button } from "../../components/ui/button";
import { useTheme } from "../../components/theme";
import { useAuth } from "../_layout";
type Role = "handyman" | "customer";
export default function RegisterScreen() {
const theme = useTheme();
const router = useRouter();
const { login } = useAuth();
const registerMutation = useMutation(api.auth.register);
const hashPassword = useAction(api.auth.hashPassword);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [password, setPassword] = useState("");
const [role, setRole] = useState<Role>("customer");
const [loading, setLoading] = useState(false);
async function handleRegister() {
if (!name || !email || !password) {
Alert.alert("Грешка", "Пополнете ги сите задолжителни полиња");
return;
}
if (password.length < 8) {
Alert.alert("Грешка", "Лозинката мора да има најмалку 8 карактери");
return;
}
setLoading(true);
try {
const passwordHash = await hashPassword({ password });
const result = await registerMutation({
name,
email,
passwordHash,
role,
phone: phone || undefined,
});
await login(result.token, result.user!._id);
router.replace("/(tabs)/(home)");
} catch (e: any) {
Alert.alert("Регистрација неуспешна", e.message || "Обидете се повторно");
} finally {
setLoading(false);
}
}
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1, backgroundColor: theme.colors.background }}
>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
contentContainerStyle={{ padding: theme.spacing.lg, gap: theme.spacing.md }}
keyboardShouldPersistTaps="handled"
>
<View style={{ marginTop: 40, marginBottom: theme.spacing.md }}>
<Text style={{ fontSize: 28, fontWeight: "700", color: theme.colors.text }}>
Креирајте профил
</Text>
<Text style={{ fontSize: 16, color: theme.colors.textSecondary, marginTop: theme.spacing.sm }}>
Изберете ја вашата улога
</Text>
</View>
<View style={{ flexDirection: "row", gap: theme.spacing.md }}>
<Pressable
onPress={() => setRole("customer")}
style={{
flex: 1,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.sm,
borderRadius: theme.radius.md,
borderWidth: 2,
borderColor: role === "customer" ? theme.colors.primary : theme.colors.border,
backgroundColor: role === "customer" ? theme.colors.primaryLight : theme.colors.surface,
alignItems: "center",
gap: theme.spacing.sm,
}}
>
<Text style={{ fontSize: 32 }}>🏠</Text>
<Text style={{ fontWeight: "600", color: theme.colors.text }}>Клиент</Text>
<Text style={{ fontSize: 12, color: theme.colors.textSecondary, textAlign: "center" }}>
Барајте мајстори
</Text>
</Pressable>
<Pressable
onPress={() => setRole("handyman")}
style={{
flex: 1,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.sm,
borderRadius: theme.radius.md,
borderWidth: 2,
borderColor: role === "handyman" ? theme.colors.primary : theme.colors.border,
backgroundColor: role === "handyman" ? theme.colors.primaryLight : theme.colors.surface,
alignItems: "center",
gap: theme.spacing.sm,
}}
>
<Text style={{ fontSize: 32 }}>🔧</Text>
<Text style={{ fontWeight: "600", color: theme.colors.text }}>Мајстор</Text>
<Text style={{ fontSize: 12, color: theme.colors.textSecondary, textAlign: "center" }}>
Нудете услуги
</Text>
</Pressable>
</View>
<Input placeholder="Име и презиме" value={name} onChangeText={setName} />
<Input
placeholder="Е-пошта"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
<Input
placeholder="Телефонски број (незадолжително)"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
/>
<Input placeholder="Лозинка (мин. 8 карактери)" value={password} onChangeText={setPassword} secureTextEntry />
<View style={{ marginTop: theme.spacing.sm }}>
<Button loading={loading} onPress={handleRegister}>
Регистрирај се
</Button>
</View>
<View style={{ flexDirection: "row", justifyContent: "center", marginTop: theme.spacing.md }}>
<Text style={{ color: theme.colors.textSecondary }}>Веќе имате профил? </Text>
<Text style={{ color: theme.colors.primary, fontWeight: "600" }} onPress={() => router.back()}>
Најавете се
</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}