Add missing lib/ (API client, auth, models) and logo asset

lib/ was never committed despite every page importing from it - a
fresh clone couldn't build. public/logo-encap.jpg was likewise
missing even though app/login/page.tsx already references it.
Also gitignore .idea/, which shouldn't be tracked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hQcuUQFue2bFvrYw2UQhK
This commit is contained in:
Linas Puplauskas 2026-08-24 17:20:22 +03:00
parent d0560df6c9
commit cb0c8dc140
6 changed files with 251 additions and 0 deletions

3
.gitignore vendored
View File

@ -36,6 +36,9 @@ yarn-error.log*
# vercel # vercel
.vercel .vercel
# IDE
.idea/
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts

175
lib/api.ts Normal file
View File

@ -0,0 +1,175 @@
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8080/api';
import { LoginResponse, User, Association } from "./models";
// Store token in localStorage (we'll improve this later)
export function setAuthToken(token: string) {
localStorage.setItem('auth_token', token);
}
export function getAuthToken(): string | null {
return localStorage.getItem('auth_token');
}
export function removeAuthToken() {
localStorage.removeItem('auth_token');
}
// Helper to add Authorization header
function getHeaders(): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
const token = getAuthToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
export async function login(email: string, password: string): Promise<LoginResponse> {
const response = await fetch(`${API_BASE_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Invalid credentials');
}
const data: LoginResponse = await response.json();
setAuthToken(data.token); // Store token
return data;
}
export async function getAssociation(id: string): Promise<Association> {
const response = await fetch(`${API_BASE_URL}/associations/${id}`, {
headers: getHeaders(),
});
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
removeAuthToken();
throw new Error('Unauthorized');
}
throw new Error('Failed to fetch association');
}
return response.json();
}
export async function getAssociations(): Promise<Association[]> {
const response = await fetch(`${API_BASE_URL}/associations`, {
headers: getHeaders(),
});
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
removeAuthToken();
throw new Error('Unauthorized');
}
throw new Error('Failed to fetch associations');
}
return response.json();
}
export async function createAssociation(association: Omit<Association, 'id' | 'created' | 'translit'>): Promise<Association> {
const response = await fetch(`${API_BASE_URL}/associations`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(association),
});
if (!response.ok) {
throw new Error('Failed to create association');
}
return response.json();
}
export async function updateAssociation(id: string, association: Association): Promise<Association> {
const response = await fetch(`${API_BASE_URL}/associations/${id}`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify(association),
});
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
removeAuthToken();
throw new Error('Unauthorized');
}
throw new Error('Failed to update association');
}
return response.json();
}
export async function downloadAttestato(id: string, associationName: string): Promise<void> {
const response = await fetch(`${API_BASE_URL}/associations/${id}/attestato`, {
headers: getHeaders(),
});
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
removeAuthToken();
throw new Error('Unauthorized');
}
throw new Error('Failed to generate attestato');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `attestato-${associationName}.pdf`;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
}
export async function deleteAssociation(id: string): Promise<void> {
const response = await fetch(`${API_BASE_URL}/associations/${id}`, {
method: 'DELETE',
headers: getHeaders(),
});
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
removeAuthToken();
throw new Error('Unauthorized');
}
throw new Error('Failed to delete association');
}
}
export function logout() {
removeAuthToken();
}
export async function verifyToken(): Promise<boolean> {
try {
const token = getAuthToken();
if (!token) return false;
const response = await fetch(`${API_BASE_URL}/associations`, {
headers: getHeaders(),
});
if (response.status === 401 || response.status === 403) {
removeAuthToken();
return false;
}
return response.ok;
} catch (error) {
console.error("verifyToken error:", error);
return false;
}
}

30
lib/auth.ts Normal file
View File

@ -0,0 +1,30 @@
"use client";
import { useEffect, useState } from "react";
import { logout as clearAuthToken } from "./api";
import type { User } from "./models";
export type StoredUser = Pick<User, "email" | "role">;
export function useStoredUser() {
const [user, setUser] = useState<StoredUser | null>(null);
const [ready, setReady] = useState(false);
useEffect(() => {
const email = localStorage.getItem("user_email");
const role = localStorage.getItem("user_role") as StoredUser["role"] | null;
if (email && role) {
setUser({ email, role });
}
setReady(true);
}, []);
return { user, ready };
}
export function clearStoredUser() {
clearAuthToken();
localStorage.removeItem("user_role");
localStorage.removeItem("user_email");
localStorage.removeItem("association_id");
}

37
lib/models.ts Normal file
View File

@ -0,0 +1,37 @@
export interface Associate {
name: string;
surname: string;
birthDate: string;
address: string;
fiscalCode: string;
italianCitizen: boolean;
role: 'PRESIDENT' | 'VICE_PRESIDENT' | 'SECRETARY' | 'ASSOCIATE';
residencePermitNumber?: string;
}
export interface User {
id?: string;
email: string;
password?: string;
role: 'CLIENT' | 'MANAGER' | 'ADMIN';
}
export interface Association {
id?: string;
runtsId?: string;
name: string;
address: string;
created?: string;
isShown?: boolean;
vatNumber?: string;
translit?: string;
associates?: Associate[];
users?: User[];
ratificationCode?: string;
}
export interface LoginResponse {
token: string;
user: User;
association: Association;
}

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

BIN
public/logo-encap.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB