Initial commit

This commit is contained in:
2026-07-09 00:14:45 +02:00
commit 0c2ec3fc1c
100 changed files with 47687 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import type { ReactNode } from 'react';
export type Role = 'USER' | 'ADMIN';
export type AuthProviderName = 'LOCAL' | 'GOOGLE' | 'FACEBOOK';
export type AccountType = 'PERSONAL' | 'COMPANY';
export type ContactPreference = 'EMAIL' | 'PHONE' | 'EMAIL_AND_PHONE';
export type PreferredLanguage = 'PL' | 'EN' | 'UK' | 'DE';
export type AuthUser = {
id: number;
email: string;
fullName: string;
role: Role;
provider: AuthProviderName;
accountType: AccountType;
phone: string | null;
address: string | null;
contactPreference: ContactPreference;
preferredLanguage: PreferredLanguage;
nip: string | null;
birthDate: string | null;
verified: boolean;
blocked: boolean;
createdAt: string;
};
type AuthResponse = { token: string; user: AuthUser };
export type RegisterDetails = {
accountType: AccountType;
phone?: string;
nip?: string;
};
type AuthContextValue = {
user: AuthUser | null;
token: string | null;
loading: boolean;
login: (email: string, password: string) => Promise<AuthUser>;
register: (email: string, password: string, fullName: string, details?: RegisterDetails) => Promise<AuthUser>;
socialLogin: (provider: Exclude<AuthProviderName, 'LOCAL'>) => Promise<AuthUser>;
updateProfile: (fullName: string, phone?: string, birthDate?: string, address?: string, contactPreference?: ContactPreference, preferredLanguage?: PreferredLanguage) => Promise<AuthUser>;
logout: () => void;
};
const TOKEN_KEY = 'polskalokalnie-auth-token';
const API_BASE = '/api';
export function getToken(): string | null {
return window.localStorage.getItem(TOKEN_KEY);
}
/**
* Cienki wrapper na fetch: dokleja token Bearer i zamienia bledy backendu (pole "message")
* na wyjatek z czytelnym komunikatem po polsku.
*/
export async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = getToken();
const headers = new Headers(options.headers);
if (options.body && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
const response = await fetch(`${API_BASE}${path}`, { ...options, headers });
if (!response.ok) {
let message = 'Wystąpił błąd. Spróbuj ponownie.';
try {
const data = await response.json();
if (data && typeof data.message === 'string' && data.message.trim()) {
message = data.message;
}
} catch {
// brak/niepoprawne cialo JSON - zostaje komunikat domyslny
}
const error = new Error(message) as Error & { status?: number };
error.status = response.status;
throw error;
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [token, setToken] = useState<string | null>(() => getToken());
const [user, setUser] = useState<AuthUser | null>(null);
const [loading, setLoading] = useState<boolean>(() => Boolean(getToken()));
// Po odswiezeniu strony: majac token w localStorage, pobierz aktualny profil.
useEffect(() => {
if (!token) {
setUser(null);
setLoading(false);
return;
}
let cancelled = false;
setLoading(true);
apiFetch<AuthUser>('/auth/me')
.then((me) => {
if (!cancelled) {
setUser(me);
setLoading(false);
}
})
.catch(() => {
if (!cancelled) {
window.localStorage.removeItem(TOKEN_KEY);
setToken(null);
setUser(null);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [token]);
const applyAuth = useCallback((auth: AuthResponse) => {
window.localStorage.setItem(TOKEN_KEY, auth.token);
setToken(auth.token);
setUser(auth.user);
return auth.user;
}, []);
const login = useCallback(
async (email: string, password: string) =>
applyAuth(await apiFetch<AuthResponse>('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})),
[applyAuth],
);
const register = useCallback(
async (email: string, password: string, fullName: string, details?: RegisterDetails) =>
applyAuth(await apiFetch<AuthResponse>('/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password, fullName, ...details }),
})),
[applyAuth],
);
const socialLogin = useCallback(
async (provider: Exclude<AuthProviderName, 'LOCAL'>) =>
applyAuth(await apiFetch<AuthResponse>('/auth/social', {
method: 'POST',
body: JSON.stringify({ provider }),
})),
[applyAuth],
);
const updateProfile = useCallback(
async (fullName: string, phone?: string, birthDate?: string, address?: string, contactPreference?: ContactPreference, preferredLanguage?: PreferredLanguage) => {
const updated = await apiFetch<AuthUser>('/auth/me', {
method: 'PUT',
body: JSON.stringify({ fullName, phone, birthDate, address, contactPreference, preferredLanguage }),
});
setUser(updated);
return updated;
},
[],
);
const logout = useCallback(() => {
window.localStorage.removeItem(TOKEN_KEY);
setToken(null);
setUser(null);
}, []);
const value = useMemo<AuthContextValue>(
() => ({ user, token, loading, login, register, socialLogin, updateProfile, logout }),
[user, token, loading, login, register, socialLogin, updateProfile, logout],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error('useAuth must be used within an AuthProvider');
}
return ctx;
}