Project Init

This commit is contained in:
Muluhabt
2026-05-29 15:23:46 +03:00
commit 2fbc557aac
67387 changed files with 6063341 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import { loginSuccess, logout as logoutAction, hydrateAuth } from '../store/auth.slice';
import type { LoginPayload } from '../types/auth.types';
export function useAuth() {
const dispatch = useAppDispatch();
const { user, token, isAuthenticated } = useAppSelector((s) => s.auth);
return {
user,
token,
isAuthenticated,
login: (p: LoginPayload) => dispatch(loginSuccess(p)),
logout: () => dispatch(logoutAction()),
hydrate: () => dispatch(hydrateAuth()),
};
}

View File

@@ -0,0 +1,89 @@
import { useState } from 'react';
import {
Paper,
TextInput,
PasswordInput,
Button,
Stack,
Title,
Center,
} from '@mantine/core';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
import { notify } from '@ema-platform/ui';
const BASE_API_URL =
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
'http://localhost:3000';
const schema = z.object({
email: z.string().email('Enter a valid email'),
password: z.string().min(6, 'Password must be at least 6 characters'),
});
type FormValues = z.infer<typeof schema>;
export function LoginPage() {
const navigate = useNavigate();
const { login } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
});
const onSubmit = async (values: FormValues) => {
setIsLoading(true);
try {
const res = await fetch(`${BASE_API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});
if (!res.ok) throw new Error('Login failed');
const data = (await res.json()) as Parameters<typeof login>[0];
login(data);
navigate('/dashboard');
} catch {
notify.error('Invalid email or password');
} finally {
setIsLoading(false);
}
};
return (
<Center h="100vh">
<Paper p="xl" shadow="md" radius="md" w={400}>
<Stack gap="md">
<Title order={3}>Sign in to Portal</Title>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="sm">
<TextInput
label="Email"
placeholder="you@example.com"
error={errors.email?.message}
{...register('email')}
/>
<PasswordInput
label="Password"
placeholder="Your password"
error={errors.password?.message}
{...register('password')}
/>
<Button type="submit" loading={isLoading} fullWidth mt="sm">
Sign in
</Button>
</Stack>
</form>
</Stack>
</Paper>
</Center>
);
}

View File

@@ -0,0 +1,42 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { AuthState, LoginPayload } from '../types/auth.types';
import { authStorage } from '../utils/auth-storage';
const initialState: AuthState = {
user: null,
token: null,
isAuthenticated: false,
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
loginSuccess(state, action: PayloadAction<LoginPayload>) {
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
authStorage.setToken(action.payload.token);
authStorage.setRefreshToken(action.payload.refreshToken);
authStorage.setUser(action.payload.user);
},
logout(state) {
state.user = null;
state.token = null;
state.isAuthenticated = false;
authStorage.clear();
},
hydrateAuth(state) {
const token = authStorage.getToken();
const user = authStorage.getUser();
if (token && user) {
state.token = token;
state.user = user;
state.isAuthenticated = true;
}
},
},
});
export const { loginSuccess, logout, hydrateAuth } = authSlice.actions;
export const authReducer = authSlice.reducer;

View File

@@ -0,0 +1,19 @@
export interface AuthUser {
id: string;
email: string;
username: string;
roles: string[];
permissions: string[];
}
export interface AuthState {
user: AuthUser | null;
token: string | null;
isAuthenticated: boolean;
}
export interface LoginPayload {
user: AuthUser;
token: string;
refreshToken: string;
}

View File

@@ -0,0 +1,23 @@
import type { AuthUser } from '../types/auth.types';
const KEYS = {
token: 'ema-portal-auth-token',
refreshToken: 'ema-portal-refresh-token',
user: 'ema-portal-auth-user',
} as const;
export const authStorage = {
getToken: () => localStorage.getItem(KEYS.token) ?? undefined,
setToken: (token: string) => localStorage.setItem(KEYS.token, token),
getRefreshToken: () => localStorage.getItem(KEYS.refreshToken) ?? undefined,
setRefreshToken: (t: string) => localStorage.setItem(KEYS.refreshToken, t),
getUser: (): AuthUser | null => {
try {
return JSON.parse(localStorage.getItem(KEYS.user) ?? 'null') as AuthUser | null;
} catch {
return null;
}
},
setUser: (u: AuthUser) => localStorage.setItem(KEYS.user, JSON.stringify(u)),
clear: () => Object.values(KEYS).forEach((k) => localStorage.removeItem(k)),
};

View File

@@ -0,0 +1,14 @@
import { Stack, Title, Text, Paper } from '@mantine/core';
export function DashboardPage() {
return (
<Stack gap="lg">
<Title order={2}>My Dashboard</Title>
<Paper p="md" shadow="sm" radius="md" withBorder>
<Text c="dimmed">
Welcome to the EMA Portal. Your content will appear here.
</Text>
</Paper>
</Stack>
);
}