feat(frontend): base pages, layout and auth scaffolding. я вижу окно авторизации, но кнопка регистрации ещё не работает
72
GEMINI.md
Normal file
|
|
@ -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.
|
||||||
16
app/app.vue
|
|
@ -1,6 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<NuxtLayout>
|
||||||
<NuxtRouteAnnouncer />
|
<NuxtPage />
|
||||||
<NuxtWelcome />
|
</NuxtLayout>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { fetchMe } = useAuth();
|
||||||
|
|
||||||
|
// Fetch the user state once on app startup.
|
||||||
|
// This call is not awaited to avoid blocking the render.
|
||||||
|
// The `initialized` guard inside `useAuth` prevents multiple calls.
|
||||||
|
fetchMe();
|
||||||
|
</script>
|
||||||
18
app/composables/useApi.ts
Normal file
|
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
101
app/composables/useAuth.ts
Normal file
|
|
@ -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>('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<User>('/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,
|
||||||
|
};
|
||||||
|
}
|
||||||
76
app/layouts/default.vue
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<div class="default-layout">
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="stats">
|
||||||
|
<span>SmurfCoins: 120</span>
|
||||||
|
<span>EXP: 850</span>
|
||||||
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<span>Lvl 5</span>
|
||||||
|
<span>Smurfette</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="app-content">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="bottom-nav">
|
||||||
|
<NuxtLink to="/village" class="nav-item">Village</NuxtLink>
|
||||||
|
<NuxtLink to="/habits" class="nav-item">Habits</NuxtLink>
|
||||||
|
<NuxtLink to="/leaderboard" class="nav-item">Leaderboard</NuxtLink>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.default-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #eef5ff;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background-color: #4a90e2;
|
||||||
|
color: white;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats, .user-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 10px 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-top: 1px solid #dcdcdc;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #4a90e2;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.router-link-exact-active {
|
||||||
|
background-color: #eef5ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
app/layouts/login.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<div class="login-layout">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-layout {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f0f4f8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
90
app/pages/habits.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<div class="habits-container">
|
||||||
|
<h3>My Habits</h3>
|
||||||
|
<div class="habits-list">
|
||||||
|
<!-- Mock Habit Card 1 -->
|
||||||
|
<div class="habit-card">
|
||||||
|
<div class="habit-info">
|
||||||
|
<h4>Practice Nuxt</h4>
|
||||||
|
<p>Active: Mon, Wed, Fri</p>
|
||||||
|
</div>
|
||||||
|
<button class="complete-btn">Complete</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mock Habit Card 2 -->
|
||||||
|
<div class="habit-card">
|
||||||
|
<div class="habit-info">
|
||||||
|
<h4>Walk 5,000 steps</h4>
|
||||||
|
<p>Active: Everyday</p>
|
||||||
|
</div>
|
||||||
|
<button class="complete-btn">Complete</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mock Habit Card 3 -->
|
||||||
|
<div class="habit-card">
|
||||||
|
<div class="habit-info">
|
||||||
|
<h4>Read a chapter</h4>
|
||||||
|
<p>Active: Tue, Thu, Sat, Sun</p>
|
||||||
|
</div>
|
||||||
|
<button class="complete-btn completed">Done</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// No logic, just visual placeholders
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.habits-container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.habits-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.habit-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.habit-info h4 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.habit-info p {
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.complete-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #88c0d0;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.complete-btn.completed {
|
||||||
|
background-color: #a3be8c;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
60
app/pages/index.vue
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<div v-if="isAuthenticated && user">
|
||||||
|
<h2>Welcome Back, {{ user.nickname }}!</h2>
|
||||||
|
<p>Your village and habits await.</p>
|
||||||
|
<div class="quick-links">
|
||||||
|
<NuxtLink to="/village">Go to Village</NuxtLink>
|
||||||
|
<NuxtLink to="/habits">Check Habits</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<!-- You can show a loading indicator here if you want -->
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { user, isAuthenticated } = useAuth();
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
// Wait until the initial fetch is done and then decide.
|
||||||
|
// isAuthenticated becomes false if the fetch fails (e.g., 401)
|
||||||
|
// or true if it succeeds. We redirect only when we are sure.
|
||||||
|
if (process.client && isAuthenticated.value === false) {
|
||||||
|
navigateTo('/login');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #4c566a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-links a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #81a1c1;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
84
app/pages/leaderboard.vue
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<div class="leaderboard-container">
|
||||||
|
<h3>Monthly Leaderboard</h3>
|
||||||
|
<ul class="leaderboard-list">
|
||||||
|
<li class="leaderboard-item">
|
||||||
|
<span class="rank">1.</span>
|
||||||
|
<span class="name">Papa Smurf</span>
|
||||||
|
<span class="exp">9800 EXP</span>
|
||||||
|
</li>
|
||||||
|
<li class="leaderboard-item self">
|
||||||
|
<span class="rank">2.</span>
|
||||||
|
<span class="name">Smurfette</span>
|
||||||
|
<span class="exp">8500 EXP</span>
|
||||||
|
</li>
|
||||||
|
<li class="leaderboard-item">
|
||||||
|
<span class="rank">3.</span>
|
||||||
|
<span class="name">Brainy Smurf</span>
|
||||||
|
<span class="exp">8250 EXP</span>
|
||||||
|
</li>
|
||||||
|
<li class="leaderboard-item">
|
||||||
|
<span class="rank">4.</span>
|
||||||
|
<span class="name">Hefty Smurf</span>
|
||||||
|
<span class="exp">7600 EXP</span>
|
||||||
|
</li>
|
||||||
|
<li class="leaderboard-item">
|
||||||
|
<span class="rank">5.</span>
|
||||||
|
<span class="name">Jokey Smurf</span>
|
||||||
|
<span class="exp">6100 EXP</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// No logic, just visual placeholders
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.leaderboard-container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-item.self {
|
||||||
|
background-color: #d8e1e9;
|
||||||
|
border: 1px solid #81a1c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4c566a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
75
app/pages/login.vue
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<h2>Smurf Habits</h2>
|
||||||
|
<form @submit.prevent>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" placeholder="papa@smurf.village" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" />
|
||||||
|
</div>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
<div class="register-link">
|
||||||
|
<p>No account? <a href="#">Register</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'login',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 350px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #4a90e2;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #4a90e2;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-link {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
59
app/pages/village.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<div class="village-container">
|
||||||
|
<h3>My Village</h3>
|
||||||
|
<div class="village-grid">
|
||||||
|
<div v-for="n in 64" :key="n" class="grid-cell">
|
||||||
|
<!-- Placeholder for village objects -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="village-actions">
|
||||||
|
<button>Build Mode</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// No logic for now, just visual placeholders
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.village-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.village-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, 1fr);
|
||||||
|
grid-template-rows: repeat(8, 1fr);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px; /* Or other size that fits your design */
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-cell {
|
||||||
|
border: 1px dotted #e0e0e0;
|
||||||
|
background-color: #a3be8c; /* Grassy color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-cell:nth-child(5) { background-color: #bf616a; } /* Fake house */
|
||||||
|
.grid-cell:nth-child(10) { background-color: #ebcb8b; } /* Fake field */
|
||||||
|
.grid-cell:nth-child(11) { background-color: #ebcb8b; } /* Fake field */
|
||||||
|
|
||||||
|
.village-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #5e81ac;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
assets/ui-references/photo_2026-01-03_13-24-52.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
assets/ui-references/photo_2026-01-03_13-25-16.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
assets/ui-references/photo_2026-01-03_13-25-21.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
17
middleware/auth.global.ts
Normal file
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
12
pages/index.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!-- /pages/index.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p v-if="user">Welcome, {{ user.nickname }}!</p>
|
||||||
|
<button @click="logout">Logout</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
</script>
|
||||||
62
pages/login.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<!-- /pages/login.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Login or Register</h1>
|
||||||
|
<form @submit.prevent="isRegistering ? handleRegister() : handleLogin()">
|
||||||
|
<div>
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" v-model="email" required />
|
||||||
|
</div>
|
||||||
|
<div v-if="isRegistering">
|
||||||
|
<label for="nickname">Nickname</label>
|
||||||
|
<input type="text" id="nickname" v-model="nickname" required />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" v-model="password" required />
|
||||||
|
</div>
|
||||||
|
<div v-if="error">{{ error }}</div>
|
||||||
|
<button type="submit" :disabled="loading">
|
||||||
|
{{ isRegistering ? 'Register' : 'Login' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<button @click="isRegistering = !isRegistering">
|
||||||
|
{{ isRegistering ? 'Switch to Login' : 'Switch to Register' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'default', // Using the default layout
|
||||||
|
});
|
||||||
|
|
||||||
|
const { login, register, loading } = useAuth();
|
||||||
|
|
||||||
|
const isRegistering = ref(false);
|
||||||
|
const email = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const nickname = ref('');
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
error.value = null;
|
||||||
|
try {
|
||||||
|
await login(email.value, password.value);
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.data?.message || 'Login failed.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
error.value = null;
|
||||||
|
try {
|
||||||
|
await register(email.value, password.value, nickname.value);
|
||||||
|
// On successful registration, switch to the login view
|
||||||
|
isRegistering.value = false;
|
||||||
|
// You might want to auto-login or show a success message instead
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.data?.message || 'Registration failed.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||