diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..39ebd3b --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,72 @@ +# GEMINI Project Context: SmurfHabits + +This document provides a comprehensive overview of the SmurfHabits project for AI-assisted development. + +## 1. Project Overview + +SmurfHabits is a mobile-first web application for habit tracking with a light gamification layer. The core user loop involves completing daily habits and quests to earn rewards, which are then used to build and develop a virtual village. This progression grants Experience Points (EXP) that are tracked on a global leaderboard. + +The project is a full-stack SPA (Single Page Application) built with Nuxt 3, using Vue 3 for the frontend and a Node.js backend powered by Nuxt Nitro. It uses SQLite as its database with Prisma ORM for data access. + +### Key Architectural Principles: +- **Monorepo:** Frontend and backend coexist in a single codebase. +- **Backend as Source of Truth:** All progression, time-based calculations, and rewards are handled by the server-side API to ensure consistency. +- **Clear Separation of Concerns:** A strict distinction is maintained between UI (in `app/`), backend logic (in `server/`), and data persistence (in `prisma/`). + +## 2. Building and Running + +The project uses `npm` for dependency management and running scripts. + +### First-Time Setup +1. **Install Node.js:** Ensure you are using a Node.js version in the 20.x LTS range. +2. **Install Dependencies:** + ```bash + npm install + ``` +3. **Setup Database:** Create the initial database and apply migrations. The `.env` file is pre-configured to use a local SQLite file (`dev.db`). + ```bash + npx prisma migrate dev + ``` + +### Development +To run the development server with hot-reloading: +```bash +npm run dev +``` + +### Building for Production +To build the application for production: +```bash +npm run build +``` + +## 3. Development Conventions + +Adhering to these conventions is critical for maintaining project stability. + +### Project Structure +- `app/`: Contains all frontend UI code (pages, components, layouts). +- `server/`: Contains all backend API code and utilities. +- `prisma/`: Contains the database schema (`schema.prisma`) and migration files. +- `public/`: Contains static assets. + +### Database and Prisma +- The project is intentionally locked to **Prisma v6.x**. Do **NOT** upgrade to v7 or introduce Prisma adapters. +- All changes to the database schema in `prisma/schema.prisma` **must** be followed by a migration command: + ```bash + npx prisma migrate dev + ``` +- The primary Prisma client instance is exported from `server/utils/prisma.ts`. Do not initialize the client elsewhere. + +### State Management +- **Frontend:** Pinia is used for client-side state management. +- **Backend:** The backend API is the authoritative source for all user progression, rewards, and time-sensitive data. The frontend should not implement its own logic for these calculations. + +### API Development +- API routes are located in `server/api/`. +- Group API endpoints by domain (e.g., `/api/habits`, `/api/village`). +- All business logic should reside in the backend. + +### AI / Gemini Usage Rules +- **DO NOT** allow the AI to change the Node.js version, upgrade Prisma, alter the Prisma configuration, or modify the core project structure. +- **ALLOWED:** The AI can be used to add or modify Prisma schema models, generate new API endpoints, and implement business logic within the existing architectural framework. diff --git a/app/app.vue b/app/app.vue index 09f935b..732c82f 100644 --- a/app/app.vue +++ b/app/app.vue @@ -1,6 +1,14 @@ + + \ No newline at end of file diff --git a/app/composables/useApi.ts b/app/composables/useApi.ts new file mode 100644 index 0000000..ba468ff --- /dev/null +++ b/app/composables/useApi.ts @@ -0,0 +1,18 @@ +// /composables/useApi.ts +export const useApi = () => { + return $fetch.create({ + baseURL: '/api', + credentials: 'include', + onRequest({ request, options }) { + // Log request + console.log('Fetching ', request, options); + }, + onResponseError({ response }) { + if (response.status === 401) { + return; // Ignore 401 unauthorized errors, they are handled by the caller. + } + // Log all other errors + console.error('Fetch error ', response.status, response._data); + }, + }); +}; diff --git a/app/composables/useAuth.ts b/app/composables/useAuth.ts new file mode 100644 index 0000000..3a3c66a --- /dev/null +++ b/app/composables/useAuth.ts @@ -0,0 +1,101 @@ +// /composables/useAuth.ts + +// Define User interface +interface User { + id: string; + email: string; + nickname: string; +} + +/** + * Composable for authentication management. + * All state is managed by Nuxt's useState to ensure it's shared and SSR-safe. + */ +export function useAuth() { + // --- State --- + // All state is defined here, inside the composable function. + // Nuxt's useState ensures this state is a singleton across the app. + const user = useState('user', () => null); + const loading = useState('auth_loading', () => false); + const initialized = useState('auth_initialized', () => false); + + // --- Composables --- + const api = useApi(); + + // --- Computed Properties --- + const isAuthenticated = computed(() => !!user.value); + + // --- Methods --- + + /** + * Fetches the current user from the backend. + * Runs only once, guarded by the 'initialized' state. + */ + const fetchMe = async () => { + if (initialized.value) { + return; + } + + loading.value = true; + initialized.value = true; + try { + const fetchedUser = await api('/auth/me', { method: 'GET' }); + user.value = fetchedUser; + } catch (error) { + user.value = null; // Silently handle 401s or other errors + } finally { + loading.value = false; + } + }; + + /** + * Logs the user in and fetches their data on success. + */ + const login = async (email, password) => { + loading.value = true; + try { + await api('/auth/login', { + method: 'POST', + body: { email, password }, + }); + + // We must re-fetch the user after logging in. + // Resetting 'initialized' allows fetchMe to run again. + initialized.value = false; + await fetchMe(); + + await navigateTo('/'); + } catch (error) { + console.error('Login failed:', error); + throw error; // Re-throw to allow the UI to handle it + } finally { + loading.value = false; + } + }; + + /** + * Logs the user out. + */ + const logout = async () => { + loading.value = true; + try { + await api('/auth/logout', { method: 'POST' }); + } catch (error) { + console.error('Logout API call failed, proceeding with client-side logout:', error); + } finally { + user.value = null; + initialized.value = false; // Allow re-fetch on next app load/login + loading.value = false; + await navigateTo('/login'); + } + }; + + return { + user, + loading, + isAuthenticated, + fetchMe, + login, + logout, + }; +} \ No newline at end of file diff --git a/app/layouts/default.vue b/app/layouts/default.vue new file mode 100644 index 0000000..083272c --- /dev/null +++ b/app/layouts/default.vue @@ -0,0 +1,76 @@ + + + diff --git a/app/layouts/login.vue b/app/layouts/login.vue new file mode 100644 index 0000000..8722bdd --- /dev/null +++ b/app/layouts/login.vue @@ -0,0 +1,15 @@ + + + diff --git a/app/pages/habits.vue b/app/pages/habits.vue new file mode 100644 index 0000000..7323f83 --- /dev/null +++ b/app/pages/habits.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/app/pages/index.vue b/app/pages/index.vue new file mode 100644 index 0000000..02a629e --- /dev/null +++ b/app/pages/index.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/app/pages/leaderboard.vue b/app/pages/leaderboard.vue new file mode 100644 index 0000000..e03c658 --- /dev/null +++ b/app/pages/leaderboard.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/app/pages/login.vue b/app/pages/login.vue new file mode 100644 index 0000000..2d4e396 --- /dev/null +++ b/app/pages/login.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/app/pages/village.vue b/app/pages/village.vue new file mode 100644 index 0000000..bba3af1 --- /dev/null +++ b/app/pages/village.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/assets/ui-references/photo_2026-01-03_13-24-52.jpg b/assets/ui-references/photo_2026-01-03_13-24-52.jpg new file mode 100644 index 0000000..b44a0a3 Binary files /dev/null and b/assets/ui-references/photo_2026-01-03_13-24-52.jpg differ diff --git a/assets/ui-references/photo_2026-01-03_13-25-16.jpg b/assets/ui-references/photo_2026-01-03_13-25-16.jpg new file mode 100644 index 0000000..a1f2ad4 Binary files /dev/null and b/assets/ui-references/photo_2026-01-03_13-25-16.jpg differ diff --git a/assets/ui-references/photo_2026-01-03_13-25-21.jpg b/assets/ui-references/photo_2026-01-03_13-25-21.jpg new file mode 100644 index 0000000..cba9fa6 Binary files /dev/null and b/assets/ui-references/photo_2026-01-03_13-25-21.jpg differ diff --git a/assets/raw/smurf1.jpg b/assets/ui-references/smurf1.jpg similarity index 100% rename from assets/raw/smurf1.jpg rename to assets/ui-references/smurf1.jpg diff --git a/assets/raw/smurf2.jpg b/assets/ui-references/smurf2.jpg similarity index 100% rename from assets/raw/smurf2.jpg rename to assets/ui-references/smurf2.jpg diff --git a/assets/raw/smurf3.jpg b/assets/ui-references/smurf3.jpg similarity index 100% rename from assets/raw/smurf3.jpg rename to assets/ui-references/smurf3.jpg diff --git a/middleware/auth.global.ts b/middleware/auth.global.ts new file mode 100644 index 0000000..ce21759 --- /dev/null +++ b/middleware/auth.global.ts @@ -0,0 +1,17 @@ +// /middleware/auth.ts +export default defineNuxtRouteMiddleware((to) => { + // `app.vue` is responsible for the initial fetchUser call. + // This middleware just reads the state that's already present. + const { isAuthenticated } = useAuth(); + + // if the user is authenticated and tries to access /login, redirect to home + if (isAuthenticated.value && to.path === '/login') { + return navigateTo('/', { replace: true }); + } + + // if the user is not authenticated and tries to access any page other than public routes, redirect to /login + const publicRoutes = ['/login', '/register']; // Add any other public paths here + if (!isAuthenticated.value && !publicRoutes.includes(to.path)) { + return navigateTo('/login', { replace: true }); + } +}); \ No newline at end of file diff --git a/pages/index.vue b/pages/index.vue new file mode 100644 index 0000000..484e61c --- /dev/null +++ b/pages/index.vue @@ -0,0 +1,12 @@ + + + + diff --git a/pages/login.vue b/pages/login.vue new file mode 100644 index 0000000..7f25d6f --- /dev/null +++ b/pages/login.vue @@ -0,0 +1,62 @@ + + + +