From 888e4e4d6032e4a422724f04b4c40ea84a2d901c Mon Sep 17 00:00:00 2001 From: Erik Silva Date: Mon, 1 Dec 2025 01:43:21 -0300 Subject: [PATCH] feat(frontend): Setup Next.js with Zustand stores and API client - Phase 2 initialization --- frontend-next/lib/api.ts | 87 +++++++ frontend-next/lib/stores/auth.store.ts | 220 ++++++++++++++++++ frontend-next/lib/stores/index.ts | 6 + frontend-next/lib/stores/tasks.store.ts | 248 ++++++++++++++++++++ frontend-next/lib/types.ts | 152 +++++++++++++ frontend-next/package-lock.json | 291 ++++++++++++++++++++++-- frontend-next/package.json | 6 +- 7 files changed, 990 insertions(+), 20 deletions(-) create mode 100644 frontend-next/lib/api.ts create mode 100644 frontend-next/lib/stores/auth.store.ts create mode 100644 frontend-next/lib/stores/index.ts create mode 100644 frontend-next/lib/stores/tasks.store.ts create mode 100644 frontend-next/lib/types.ts diff --git a/frontend-next/lib/api.ts b/frontend-next/lib/api.ts new file mode 100644 index 0000000..39a8fb7 --- /dev/null +++ b/frontend-next/lib/api.ts @@ -0,0 +1,87 @@ +/** + * šŸ”Œ API Client Configuration + * Cliente HTTP para comunicação com backend + */ + +import axios, { AxiosInstance, AxiosError } from 'axios'; +import { ApiError, TaskApiError } from './types'; + +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api'; +const API_TIMEOUT = parseInt(process.env.NEXT_PUBLIC_API_TIMEOUT || '10000'); + +/** + * InstĆ¢ncia Axios configurada + */ +const apiClient: AxiosInstance = axios.create({ + baseURL: API_URL, + timeout: API_TIMEOUT, + headers: { + 'Content-Type': 'application/json', + }, +}); + +/** + * Interceptor: Adicionar token JWT Ć s requisiƧƵes + */ +apiClient.interceptors.request.use( + (config) => { + // Obter token do localStorage + const token = + typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null; + + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + + return config; + }, + (error) => { + return Promise.reject(error); + }, +); + +/** + * Interceptor: Tratar respostas e erros + */ +apiClient.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + const status = error.response?.status; + const data = error.response?.data as any; + + // Logout em caso de token invĆ”lido + if (status === 401) { + localStorage.removeItem('auth_token'); + localStorage.removeItem('auth_user'); + window.dispatchEvent(new Event('auth:logout')); + } + + // LanƧar erro customizado + throw new TaskApiError( + status || 500, + data?.message || error.message || 'Erro na API', + error, + ); + }, +); + +/** + * Helper: tratamento de erros + */ +export const handleApiError = (error: any): string => { + if (error instanceof TaskApiError) { + return error.message; + } + + if (error instanceof axios.AxiosError) { + return ( + (error.response?.data as any)?.message || + error.message || + 'Erro na requisição' + ); + } + + return 'Erro desconhecido'; +}; + +export default apiClient; diff --git a/frontend-next/lib/stores/auth.store.ts b/frontend-next/lib/stores/auth.store.ts new file mode 100644 index 0000000..aea1922 --- /dev/null +++ b/frontend-next/lib/stores/auth.store.ts @@ -0,0 +1,220 @@ +/** + * šŸ” Auth Store (Zustand) + * Gerenciador de estado de autenticação + */ + +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import apiClient, { handleApiError } from '../api'; +import { + AuthState, + AuthUser, + SignupPayload, + LoginPayload, + AuthResponse, +} from '../types'; + +interface AuthStore extends AuthState { + // aƧƵes + signup: (payload: SignupPayload) => Promise; + login: (payload: LoginPayload) => Promise; + logout: () => Promise; + getProfile: () => Promise; + setError: (error: string | null) => void; + setLoading: (loading: boolean) => void; +} + +/** + * Decodificar JWT para pegar o userId + */ +const decodeToken = (token: string): { userId: string } | null => { + try { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64) + .split('') + .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join(''), + ); + return JSON.parse(jsonPayload); + } catch { + return null; + } +}; + +/** + * Criar store de autenticação + */ +export const useAuthStore = create()( + persist( + (set, get) => ({ + // Estado inicial + user: null, + token: null, + isAuthenticated: false, + isLoading: false, + error: null, + + // ============================================================================ + // AƇƕES + // ============================================================================ + + /** + * Registrar novo usuĆ”rio + */ + signup: async (payload: SignupPayload) => { + try { + set({ isLoading: true, error: null }); + + const response = await apiClient.post( + '/auth/signup', + payload, + ); + const { access_token, user } = response.data; + + set({ + token: access_token, + user, + isAuthenticated: true, + isLoading: false, + }); + + // Salvar token no localStorage + localStorage.setItem('auth_token', access_token); + localStorage.setItem('auth_user', JSON.stringify(user)); + } catch (error) { + const message = handleApiError(error); + set({ error: message, isLoading: false }); + throw new Error(message); + } + }, + + /** + * Fazer login + */ + login: async (payload: LoginPayload) => { + try { + set({ isLoading: true, error: null }); + + const response = await apiClient.post( + '/auth/login', + payload, + ); + const { access_token, user } = response.data; + + set({ + token: access_token, + user, + isAuthenticated: true, + isLoading: false, + }); + + // Salvar no localStorage + localStorage.setItem('auth_token', access_token); + localStorage.setItem('auth_user', JSON.stringify(user)); + } catch (error) { + const message = handleApiError(error); + set({ error: message, isLoading: false }); + throw new Error(message); + } + }, + + /** + * Fazer logout + */ + logout: async () => { + try { + set({ isLoading: true }); + // Chamar endpoint de logout (opcional) + await apiClient.post('/auth/logout'); + } catch (error) { + console.error('Erro ao fazer logout:', error); + } finally { + // Limpar estado e localStorage + set({ + user: null, + token: null, + isAuthenticated: false, + isLoading: false, + error: null, + }); + + localStorage.removeItem('auth_token'); + localStorage.removeItem('auth_user'); + localStorage.removeItem('auth_store'); + } + }, + + /** + * Obter perfil do usuĆ”rio + */ + getProfile: async () => { + try { + const response = await apiClient.get('/auth/me'); + const user = response.data; + + set({ user, isAuthenticated: true }); + localStorage.setItem('auth_user', JSON.stringify(user)); + + return user; + } catch (error) { + const message = handleApiError(error); + set({ error: message, isAuthenticated: false }); + return null; + } + }, + + /** + * Setar erro + */ + setError: (error: string | null) => { + set({ error }); + }, + + /** + * Setar loading + */ + setLoading: (isLoading: boolean) => { + set({ isLoading }); + }, + }), + + // Configurar persistĆŖncia + { + name: 'auth_store', + partialize: (state) => ({ + token: state.token, + user: state.user, + isAuthenticated: state.isAuthenticated, + }), + + // Validar token ao recuperar do storage + onRehydrateStorage: () => (state) => { + if (state?.token) { + const decoded = decodeToken(state.token); + if (!decoded) { + state.token = null; + state.isAuthenticated = false; + } + } + }, + }, + ), +); + +// Exportar hook separado para debugging +export const useAuth = () => { + const store = useAuthStore(); + return { + user: store.user, + token: store.token, + isAuthenticated: store.isAuthenticated, + isLoading: store.isLoading, + error: store.error, + login: store.login, + signup: store.signup, + logout: store.logout, + getProfile: store.getProfile, + }; +}; diff --git a/frontend-next/lib/stores/index.ts b/frontend-next/lib/stores/index.ts new file mode 100644 index 0000000..8a37978 --- /dev/null +++ b/frontend-next/lib/stores/index.ts @@ -0,0 +1,6 @@ +/** + * šŸ›’ Store Exports + */ + +export { useAuthStore, useAuth } from './auth.store'; +export { useTasksStore, useTasks } from './tasks.store'; diff --git a/frontend-next/lib/stores/tasks.store.ts b/frontend-next/lib/stores/tasks.store.ts new file mode 100644 index 0000000..d3fc6f6 --- /dev/null +++ b/frontend-next/lib/stores/tasks.store.ts @@ -0,0 +1,248 @@ +/** + * šŸ“ Tasks Store (Zustand) + * Gerenciador de estado das tarefas + */ + +import { create } from 'zustand'; +import apiClient, { handleApiError } from '../api'; +import { + TasksState, + Task, + CreateTaskPayload, + UpdateTaskPayload, + TaskFilters, + TasksListResponse, + TaskStatsResponse, +} from '../types'; + +interface TasksStore extends TasksState { + // AƧƵes + fetchTasks: (filters?: TaskFilters) => Promise; + fetchStats: () => Promise; + createTask: (payload: CreateTaskPayload) => Promise; + updateTask: (id: string, payload: UpdateTaskPayload) => Promise; + deleteTask: (id: string) => Promise; + setFilters: (filters: TaskFilters) => void; + setError: (error: string | null) => void; + setLoading: (loading: boolean) => void; + clear: () => void; +} + +/** + * Criar store de tarefas + */ +export const useTasksStore = create((set, get) => ({ + // Estado inicial + tasks: [], + stats: null, + isLoading: false, + error: null, + filters: { + completed: undefined, + sortBy: 'created_at', + order: 'desc', + }, + + // ============================================================================ + // AƇƕES + // ============================================================================ + + /** + * Buscar tarefas com filtros + */ + fetchTasks: async (filters?: TaskFilters) => { + try { + set({ isLoading: true, error: null }); + + if (filters) { + set({ filters }); + } + + const currentFilters = filters || get().filters; + const params = new URLSearchParams(); + + if (currentFilters.completed !== undefined) { + params.append('completed', String(currentFilters.completed)); + } + if (currentFilters.category) { + params.append('category', currentFilters.category); + } + if (currentFilters.priority) { + params.append('priority', currentFilters.priority); + } + if (currentFilters.sortBy) { + params.append('sortBy', currentFilters.sortBy); + } + if (currentFilters.order) { + params.append('order', currentFilters.order); + } + + const url = `/tasks${params.toString() ? '?' + params.toString() : ''}`; + const response = await apiClient.get(url); + + set({ + tasks: response.data.data, + isLoading: false, + }); + } catch (error) { + const message = handleApiError(error); + set({ error: message, isLoading: false, tasks: [] }); + } + }, + + /** + * Buscar estatĆ­sticas + */ + fetchStats: async () => { + try { + const response = await apiClient.get('/tasks/stats'); + set({ stats: response.data.data }); + } catch (error) { + console.error('Erro ao buscar estatĆ­sticas:', error); + } + }, + + /** + * Criar tarefa + */ + createTask: async (payload: CreateTaskPayload) => { + try { + set({ isLoading: true, error: null }); + + const response = await apiClient.post<{ data: Task }>('/tasks', payload); + const newTask = response.data.data; + + // Adicionar Ć  lista + set({ tasks: [newTask, ...get().tasks] }); + + // Atualizar stats se houver + if (get().stats) { + await get().fetchStats(); + } + + return newTask; + } catch (error) { + const message = handleApiError(error); + set({ error: message }); + throw new Error(message); + } finally { + set({ isLoading: false }); + } + }, + + /** + * Atualizar tarefa + */ + updateTask: async (id: string, payload: UpdateTaskPayload) => { + try { + set({ isLoading: true, error: null }); + + const response = await apiClient.patch<{ data: Task }>( + `/tasks/${id}`, + payload, + ); + const updatedTask = response.data.data; + + // Atualizar na lista + set({ + tasks: get().tasks.map((t) => (t.id === id ? updatedTask : t)), + }); + + // Atualizar stats se houver + if (get().stats) { + await get().fetchStats(); + } + + return updatedTask; + } catch (error) { + const message = handleApiError(error); + set({ error: message }); + throw new Error(message); + } finally { + set({ isLoading: false }); + } + }, + + /** + * Deletar tarefa + */ + deleteTask: async (id: string) => { + try { + set({ isLoading: true, error: null }); + + await apiClient.delete(`/tasks/${id}`); + + // Remover da lista + set({ tasks: get().tasks.filter((t) => t.id !== id) }); + + // Atualizar stats se houver + if (get().stats) { + await get().fetchStats(); + } + + return true; + } catch (error) { + const message = handleApiError(error); + set({ error: message }); + return false; + } finally { + set({ isLoading: false }); + } + }, + + /** + * Setar filtros + */ + setFilters: (filters: TaskFilters) => { + set({ filters }); + }, + + /** + * Setar erro + */ + setError: (error: string | null) => { + set({ error }); + }, + + /** + * Setar loading + */ + setLoading: (isLoading: boolean) => { + set({ isLoading }); + }, + + /** + * Limpar estado + */ + clear: () => { + set({ + tasks: [], + stats: null, + isLoading: false, + error: null, + filters: { + completed: undefined, + sortBy: 'created_at', + order: 'desc', + }, + }); + }, +})); + +// Hook customizado +export const useTasks = () => { + const store = useTasksStore(); + return { + tasks: store.tasks, + stats: store.stats, + isLoading: store.isLoading, + error: store.error, + filters: store.filters, + fetchTasks: store.fetchTasks, + fetchStats: store.fetchStats, + createTask: store.createTask, + updateTask: store.updateTask, + deleteTask: store.deleteTask, + setFilters: store.setFilters, + }; +}; diff --git a/frontend-next/lib/types.ts b/frontend-next/lib/types.ts new file mode 100644 index 0000000..fc320af --- /dev/null +++ b/frontend-next/lib/types.ts @@ -0,0 +1,152 @@ +/** + * šŸ“‹ Task Manager - TypeScript Types + * Tipos compartilhados entre frontend e backend + */ + +// ============================================================================ +// šŸ” AUTENTICAƇƃO +// ============================================================================ + +export interface AuthUser { + id: string; + email: string; + created_at: string; + email_confirmed_at: string | null; +} + +export interface SignupPayload { + email: string; + password: string; + name?: string; +} + +export interface LoginPayload { + email: string; + password: string; +} + +export interface AuthResponse { + access_token: string; + user: AuthUser; +} + +export interface JWTPayload { + userId: string; + email: string; + iat: number; + exp: number; +} + +// ============================================================================ +// šŸ“ TAREFAS (TASKS) +// ============================================================================ + +export interface Task { + id: string; + user_id: string; + title: string; + description: string | null; + completed: boolean; + due_date: string | null; + category: string | null; + priority: 'low' | 'medium' | 'high'; + created_at: string; + updated_at: string; +} + +export interface CreateTaskPayload { + title: string; + description?: string; + dueDate?: string; + category?: string; + priority?: 'low' | 'medium' | 'high'; + completed?: boolean; +} + +export interface UpdateTaskPayload { + title?: string; + description?: string; + dueDate?: string; + category?: string; + priority?: 'low' | 'medium' | 'high'; + completed?: boolean; +} + +export interface TaskFilters { + completed?: boolean; + category?: string; + priority?: 'low' | 'medium' | 'high'; + sortBy?: 'created_at' | 'due_date' | 'priority'; + order?: 'asc' | 'desc'; +} + +export interface TaskStats { + total: number; + completed: number; + pending: number; + completionPercentage: number; +} + +// ============================================================================ +// šŸ“Š API RESPONSES +// ============================================================================ + +export interface ApiResponse { + success: boolean; + message: string; + data?: T; +} + +export interface TasksListResponse { + success: boolean; + message: string; + count: number; + data: Task[]; +} + +export interface TaskStatsResponse { + success: boolean; + message: string; + data: TaskStats; +} + +// ============================================================================ +// āŒ ERROS +// ============================================================================ + +export interface ApiError { + statusCode: number; + message: string; + error?: string; +} + +export class TaskApiError extends Error { + constructor( + public statusCode: number, + message: string, + public originalError?: any, + ) { + super(message); + this.name = 'TaskApiError'; + } +} + +// ============================================================================ +// šŸ›’ STORE STATE +// ============================================================================ + +export interface AuthState { + user: AuthUser | null; + token: string | null; + isAuthenticated: boolean; + isLoading: boolean; + error: string | null; +} + +export interface TasksState { + tasks: Task[]; + stats: TaskStats | null; + isLoading: boolean; + error: string | null; + filters: TaskFilters; +} diff --git a/frontend-next/package-lock.json b/frontend-next/package-lock.json index 25db141..559e52d 100644 --- a/frontend-next/package-lock.json +++ b/frontend-next/package-lock.json @@ -8,9 +8,13 @@ "name": "frontend-next", "version": "0.1.0", "dependencies": { + "@supabase/supabase-js": "^2.86.0", + "axios": "^1.13.2", + "lucide-react": "^0.555.0", "next": "16.0.6", "react": "19.2.0", - "react-dom": "19.2.0" + "react-dom": "19.2.0", + "zustand": "^5.0.9" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -1234,6 +1238,86 @@ "dev": true, "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.86.0.tgz", + "integrity": "sha512-3xPqMvBWC6Haqpr6hEWmSUqDq+6SA1BAEdbiaHdAZM9QjZ5uiQJ+6iD9pZOzOa6MVXZh4GmwjhC9ObIG0K1NcA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.86.0.tgz", + "integrity": "sha512-AlOoVfeaq9XGlBFIyXTmb+y+CZzxNO4wWbfgRM6iPpNU5WCXKawtQYSnhivi3UVxS7GA0rWovY4d6cIAxZAojA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.86.0.tgz", + "integrity": "sha512-QVf+wIXILcZJ7IhWhWn+ozdf8B+oO0Ulizh2AAPxD/6nQL+x3r9lJ47a+fpc/jvAOGXMbkeW534Kw6jz7e8iIA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.86.0.tgz", + "integrity": "sha512-dyS8bFoP29R/sj5zLi0AP3JfgG8ar1nuImcz5jxSx7UIW7fbFsXhUCVrSY2Ofo0+Ev6wiATiSdBOzBfWaiFyPA==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.86.0.tgz", + "integrity": "sha512-PM47jX/Mfobdtx7NNpoj9EvlrkapAVTQBZgGGslEXD6NS70EcGjhgRPBItwHdxZPM5GwqQ0cGMN06uhjeY2mHQ==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.86.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.86.0.tgz", + "integrity": "sha512-BaC9sv5+HGNy1ulZwY8/Ev7EjfYYmWD4fOMw9bDBqTawEj6JHAiOHeTwXLRzVaeSay4p17xYLN2NSCoGgXMQnw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.86.0", + "@supabase/functions-js": "2.86.0", + "@supabase/postgrest-js": "2.86.0", + "@supabase/realtime-js": "2.86.0", + "@supabase/storage-js": "2.86.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1550,17 +1634,22 @@ "version": "20.19.25", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -1577,6 +1666,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.48.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", @@ -2368,6 +2466,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2394,6 +2498,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2503,7 +2618,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2603,6 +2717,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2636,7 +2762,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2761,6 +2887,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2788,7 +2923,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2900,7 +3034,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2910,7 +3043,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2948,7 +3080,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2961,7 +3092,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3219,6 +3349,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3577,6 +3708,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3593,11 +3744,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3658,7 +3824,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3683,7 +3848,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3771,7 +3935,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3850,7 +4013,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3863,7 +4025,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3879,7 +4040,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3905,6 +4065,15 @@ "hermes-estree": "0.25.1" } }, + "node_modules/iceberg-js": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.0.tgz", + "integrity": "sha512-kmgmea2nguZEvRqW79gDqNXyxA3OS5WIgMVffrHpqXV4F/J4UmNIw2vstixioLTNSkd5rFB8G0s3Lwzogm6OFw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4848,6 +5017,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.555.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.555.0.tgz", + "integrity": "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4862,7 +5040,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4892,6 +5069,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5361,6 +5559,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6315,7 +6519,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -6509,6 +6712,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6552,6 +6776,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", + "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/frontend-next/package.json b/frontend-next/package.json index 9f0c929..e5e38eb 100644 --- a/frontend-next/package.json +++ b/frontend-next/package.json @@ -9,9 +9,13 @@ "lint": "eslint" }, "dependencies": { + "@supabase/supabase-js": "^2.86.0", + "axios": "^1.13.2", + "lucide-react": "^0.555.0", "next": "16.0.6", "react": "19.2.0", - "react-dom": "19.2.0" + "react-dom": "19.2.0", + "zustand": "^5.0.9" }, "devDependencies": { "@tailwindcss/postcss": "^4",