feat: initial backend MVP (auth, habits, village, leaderboard)

This commit is contained in:
Alexander Andreev 2026-01-02 16:09:05 +03:00
parent f0177d31c0
commit de30f96c57
19 changed files with 11629 additions and 3 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
/app/generated/prisma
prisma/dev.db

109
GEMINI/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,109 @@
# Architecture
## 1. High-Level Architecture
The application follows a fullstack SPA architecture:
- Nuxt 3 frontend
- Nitro backend (Node.js)
- REST API
- MySQL database
- Prisma ORM
Frontend and backend live in a single repository.
---
## 2. Architectural Principles
- Clear separation of concerns:
- UI
- State
- Domain logic
- Persistence
- Backend is the source of truth
- Frontend does not calculate time-based progression
- No real-time connections (WebSockets not required)
---
## 3. Frontend Structure
- Pages:
- Habits
- Quests
- Village
- Leaderboard
- Components:
- HabitCard
- QuestItem
- VillageGrid
- VillageObject
- BuildModeOverlay
- State:
- userStore
- habitsStore
- questsStore
- villageStore
- leaderboardStore
---
## 4. Backend Structure
- API routes grouped by domain:
- /auth
- /user
- /habits
- /quests
- /village
- /leaderboard
- Domain services:
- HabitService
- QuestService
- VillageService
- CropGrowthService
- All time-based logic is calculated on request
---
## 5. Village Logic
- Village grid is logical (cell-based)
- Isometric view is purely visual
- Build mode:
- shows grid overlay
- allows placing, moving and removing objects
- View mode:
- allows planting and harvesting
- Obstacles must be cleared before building
---
## 6. Time & Progression
- Server calculates:
- whether a quest is available today
- whether a crop is grown
- streak progression
- Time is based on user local date stored on server
- No background jobs required for MVP
---
## 7. Data Flow
1. Frontend requests current state
2. Backend recalculates progression if needed
3. Backend returns updated state
4. Frontend updates Pinia stores
5. UI reflects the new state
---
## 8. Non-Goals
- No SSR optimization
- No WebSocket connections
- No offline mode
- No push notifications

View File

@ -0,0 +1,156 @@
# Business Requirements
## 1. Product Goal
Create a mobile-first web application for habit tracking with a light game layer.
Core idea:
Daily habits → quests → rewards → village development → EXP → leaderboard.
The project is an MVP / pet project.
No monetization, no anti-cheat, no social pressure mechanics.
---
## 2. Target Audience
- Users who want to build daily habits
- Users who like visual progress and game mechanics
- Mobile users as primary platform
---
## 3. Core User Loop
1. User opens the app
2. Completes a habit or daily quest
3. Receives coins
4. Spends coins on village development
5. Collects crops and gains EXP
6. Sees progress and leaderboard position
7. Returns the next day
---
## 4. Functional Requirements
### 4.1 User & Profile
- Registration and login via email + password
- One email = one account
- Email confirmation is NOT required (MVP)
- Profile contains:
- public nickname
- public avatar
- settings:
- sound on/off
- confetti on/off
---
### 4.2 Habits
- Maximum 3 habits per user
- Habit fields:
- name (custom or predefined)
- active days of week
- Habits are:
- permanent
- editable
- removable
- Missed days:
- are not penalized
- shown as red cells in calendar
- Completed days:
- shown as green cells
---
### 4.3 Quests
#### Habit quests
- Can be completed only on active days
- Reward: 3 coins per completion
#### Daily quest
- “I visited the site today”
- Can be completed once per day
- Reward: 1 coin
#### Streak
- 5 consecutive daily visits → +10 coins
- Streak resets after reward
#### UX
- Quest completion triggers:
- light confetti animation
- short success sound
- Both effects can be disabled in settings
---
### 4.4 Village
- 2D isometric grid
- Grid fits into one mobile screen (no scroll)
- Two modes:
- view mode
- build mode
#### Objects
- House (1 house = 1 worker)
- Field
- Road
- Fence
- Obstacles (rocks, bushes, mushrooms)
#### Rules
- Fields cannot exceed number of workers
- Removing objects does NOT refund coins
- Removing houses blocks building new fields but does not remove existing ones
---
### 4.5 Crops & EXP
- Crop types:
- Blueberries
- Corn (grows longer)
- Growth:
- real-time based
- no acceleration mechanics
- Harvest:
- manual
- does not expire
- Rewards:
- harvesting gives EXP
- corn additionally gives +1 coin
---
### 4.6 Leaderboard
- Global leaderboard
- Period: monthly
- Sorted by EXP
- Shows:
- rank
- avatar
- nickname
- EXP
- Equal EXP results in shared ranks
(e.g. 1, 2, 2, 2, 3, 4, 5, 5)
---
## 5. MVP Exclusions
The MVP explicitly excludes:
- donations or payments
- levels
- progress acceleration
- social features
- chat
- push notifications
- anti-cheat
- email verification

65
GEMINI/TECH_STACK.md Normal file
View File

@ -0,0 +1,65 @@
# Technical Stack
## 1. General Approach
- Monorepo
- Single codebase for frontend and backend
- SPA (no SEO requirements)
- Mobile-first
- Backend used as API and calculation layer
---
## 2. Frontend
- Nuxt 3
- Vue 3
- TypeScript
- Pinia (state management)
- Vite (build tool)
- CSS or Tailwind CSS (implementation choice)
---
## 3. Backend
- Node.js via Nuxt Nitro
- REST API (no GraphQL)
- Server-side calculation for:
- crop growth
- quest availability
- streaks
- rewards
---
## 4. Database
- SQLite
- Prisma ORM
- Migrations required
- Seed data required (initial crops, obstacles, presets)
---
## 5. State Management
- Pinia as a single source of truth on frontend
- Server is authoritative for:
- time-based logic
- rewards
- progression
---
## 6. Internationalization
- i18n support enabled from start
- Default language: RU
- EN prepared for future use
---
## 7. Testing
- Unit tests are NOT required for MVP

214
README.md Normal file
View File

@ -0,0 +1,214 @@
# Smurf Habits
Habit tracker with a light game layer (village, quests, EXP).
Mobile-first web application.
---
## 1. Tech Stack
- Node.js **20 LTS** (required)
- Nuxt **4.x**
- Vue 3
- Prisma **6.x** (⚠️ NOT 7)
- SQLite (development & MVP)
- TypeScript
---
## 2. Environment Requirements
### Node.js
**Required:**
```
Node >= 20.x (LTS)
```
❌ Node 22 / 24 are NOT supported
❌ Do not use experimental Node versions
Check:
```bash
node -v
```
---
## 3. Project Structure
```text
/
├─ app/ # UI (pages, components, layouts)
├─ server/ # Backend (API, utils, Prisma)
│ ├─ api/
│ └─ utils/
├─ prisma/
│ ├─ schema.prisma
│ └─ migrations/
├─ public/
├─ .env
├─ nuxt.config.ts
└─ README.md
```
### Important rules
- `app/` — UI only
- `server/` — backend only
- `server/` MUST be in project root (not inside `app/`)
- Do NOT change this structure
---
## 4. Prisma Setup (IMPORTANT)
### Prisma version
This project **intentionally uses Prisma 6**.
❌ Do NOT upgrade to Prisma 7
❌ Do NOT use Prisma adapters
❌ Do NOT remove `DATABASE_URL`
Reason:
- Prisma 7 has unstable adapter-based API
- Prisma 6 is stable and well-supported by Nuxt and tooling
---
### Prisma schema
`prisma/schema.prisma` uses classic datasource config:
```prisma
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
```
---
### Environment variables
`.env`:
```env
DATABASE_URL="file:./dev.db"
```
---
### Prisma workflow
Whenever you change `schema.prisma`:
```bash
npx prisma migrate dev
```
Never forget migrations.
---
## 5. Prisma Client Usage
Prisma client is initialized here:
```ts
server/utils/prisma.ts
```
```ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default prisma
```
### Rules
- Do NOT initialize PrismaClient elsewhere
- Do NOT use dynamic imports
- Do NOT change this file without a good reason
---
## 6. Development
Install dependencies:
```bash
npm install
```
Generate Prisma client:
```bash
npx prisma generate
```
Run dev server:
```bash
npm run dev
```
---
## 7. API Example
Health check:
```
GET /api/health
```
Expected response:
```json
{
"ok": true,
"usersCount": 0
}
```
---
## 8. Deployment Notes
- Use Node 20 on hosting
- Run Prisma migrations during deployment
- SQLite is acceptable for MVP
- Database file: `dev.db`
---
## 9. AI / Gemini Rules (IMPORTANT)
When using Gemini / AI tools:
**DO NOT ALLOW:**
- changing Node version
- upgrading Prisma
- changing Prisma configuration
- modifying project structure
**ALLOWED:**
- adding models to `schema.prisma`
- generating API endpoints
- implementing business logic
---
## 10. Why these constraints exist
This setup was intentionally chosen to:
- avoid unstable Prisma 7 API
- keep development predictable
- ensure compatibility with Nuxt and Node
- prevent tooling-related regressions
Breaking these rules will likely break the project.

6
app/app.vue Normal file
View File

@ -0,0 +1,6 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtWelcome />
</div>
</template>

BIN
assets/raw/smurf1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
assets/raw/smurf2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
assets/raw/smurf3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

5
nuxt.config.ts Normal file
View File

@ -0,0 +1,5 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true }
})

10892
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "smurfhabits",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@prisma/client": "^6.19.1",
"nuxt": "^4.2.2",
"prisma": "^6.19.1",
"vue": "^3.5.26",
"vue-router": "^4.6.4"
}
}

14
prisma.config.ts Normal file
View File

@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});

View File

@ -0,0 +1,88 @@
/*
Warnings:
- Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- CreateTable
CREATE TABLE "Habit" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"daysOfWeek" JSONB NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "Habit_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "HabitCompletion" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"date" DATETIME NOT NULL,
"habitId" INTEGER NOT NULL,
CONSTRAINT "HabitCompletion_habitId_fkey" FOREIGN KEY ("habitId") REFERENCES "Habit" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "DailyVisit" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"date" DATETIME NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "DailyVisit_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Village" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"userId" INTEGER NOT NULL,
CONSTRAINT "Village_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "VillageObject" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"type" TEXT NOT NULL,
"x" INTEGER NOT NULL,
"y" INTEGER NOT NULL,
"obstacleMetadata" TEXT,
"cropType" TEXT,
"plantedAt" DATETIME,
"villageId" INTEGER NOT NULL,
CONSTRAINT "VillageObject_villageId_fkey" FOREIGN KEY ("villageId") REFERENCES "Village" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"nickname" TEXT,
"avatar" TEXT DEFAULT '/avatars/default.png',
"coins" INTEGER NOT NULL DEFAULT 0,
"exp" INTEGER NOT NULL DEFAULT 0,
"soundOn" BOOLEAN NOT NULL DEFAULT true,
"confettiOn" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_User" ("createdAt", "email", "id") SELECT "createdAt", "email", "id" FROM "User";
DROP TABLE "User";
ALTER TABLE "new_User" RENAME TO "User";
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
-- CreateIndex
CREATE UNIQUE INDEX "HabitCompletion_habitId_date_key" ON "HabitCompletion"("habitId", "date");
-- CreateIndex
CREATE UNIQUE INDEX "DailyVisit_userId_date_key" ON "DailyVisit"("userId", "date");
-- CreateIndex
CREATE UNIQUE INDEX "Village_userId_key" ON "Village"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "VillageObject_villageId_x_y_key" ON "VillageObject"("villageId", "x", "y");

View File

@ -1,3 +1,12 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// prisma/schema.prisma // prisma/schema.prisma
// Enums // Enums
@ -61,7 +70,7 @@ model Habit {
// This creates a history of the user's progress for each habit. // This creates a history of the user's progress for each habit.
model HabitCompletion { model HabitCompletion {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
date DateTime @db.Date // Store only the date part date DateTime // Store only the date part
// Relations // Relations
habit Habit @relation(fields: [habitId], references: [id], onDelete: Cascade) habit Habit @relation(fields: [habitId], references: [id], onDelete: Cascade)
@ -74,7 +83,7 @@ model HabitCompletion {
// quest and for calculating 5-day streaks. // quest and for calculating 5-day streaks.
model DailyVisit { model DailyVisit {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
date DateTime @db.Date // Store only the date part date DateTime // Store only the date part
// Relations // Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-Agent: *
Disallow:

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
}