ali-ahnaf / ali-ahnaf/pocket_pixel

Add a new page called Adventures, where users can plan and track the gold they'll spend on a trip or outing.

Aperta
#244 6 commenti 0 reazioni 1 assegnatario Rivendicata da @rahulkatiayr Vedi su GitHub
backend frontend up for grabs
Lingua principale
TypeScript
Stelle
14
Fork
91
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Descrizione

Right now there's no way to plan a one-off trip or event across multiple
expenses. Vaults are ongoing spending categories, not a "planning" tool for a
single bounded adventure with its own budget and item list.

## 📖 The Incantation

### Theme
Frame the page like preparing for a quest:
- Creating an Adventure = "Charting a new quest"
- The total cost = **Adventure Fund** / **Quest Chest**
- Each Adventure can have flavor text, e.g. *"A journey into the misty peaks —
pack wisely, adventurer."*
- A finished/past Adventure shows a "Quest Complete" summary with total gold spent

### Data Models (TypeORM, following `Vault.entity.ts` conventions)

**`Adventure.entity.ts`**
```ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
import { User } from './User.entity';
import { AdventureItem } from './AdventureItem.entity';
import { BaseEntity } from './BaseEntity';

@Entity('adventures')
export class Adventure extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'varchar' })
userId: string;

@Column({ type: 'varchar', length: 100 })
name: string;

@Column({ type: 'varchar', length: 255, nullable: true })
description: string | null;

@Column({ type: 'varchar', length: 100, nullable: true })
icon: string | null;

@Column({ type: 'varchar', length: 50, nullable: true })
backgroundColor: string | null;

@Column({ type: 'date', nullable: true })
startDate: string | null;

@Column({ type: 'date', nullable: true })
endDate: string | null;

@Column({ type: 'boolean', default: false })
isComplete: boolean;

@ManyToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'userId' })
user: User;

@OneToMany(() => AdventureItem, (item) => item.adventure)
items: AdventureItem[];
}
```

**`AdventureItem.entity.ts`**
```ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { Adventure } from './Adventure.entity';
import { BaseEntity } from './BaseEntity';

@Entity('adventure_items')
export class AdventureItem extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'varchar' })
adventureId: string;

@Column({ type: 'varchar', length: 150 })
name: string;

@Column({ type: 'decimal', precision: 10, scale: 2 })
amount: number;

@ManyToOne(() => Adventure, (adventure) => adventure.items, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'adventureId' })
adventure: Adventure;
}
```

### API Endpoints (mirroring `vaults.routes.ts` folder structure)

**Adventures** — `packages/api/src/routes/adventures/`

| Method | Route | Description |
|---|---|---|
| GET | `/adventures` | List all adventures for the logged-in user |
| GET | `/adventures/:adventureId` | Get one adventure with its items and total |
| POST | `/adventures` | Create a new adventure (`name`, `description?`, `icon?`, `backgroundColor?`, `startDate?`, `endDate?`) |
| PUT | `/adventures/:adventureId` | Update adventure details / mark `isComplete` |
| DELETE | `/adventures/:adventureId` | Delete an adventure (cascades to its items) |

**Adventure Items** — `packages/api/src/routes/adventures/items/`

| Method | Route | Description |
|---|---|---|
| GET | `/adventures/:adventureId/items` | List items for an adventure |
| POST | `/adventures/:adventureId/items` | Add an item (`name`, `amount`) |
| PUT | `/adventures/:adventureId/items/:itemId` | Update an item's name/amount |
| DELETE | `/adventures/:adventureId/items/:itemId` | Remove an item |

Validation via Joi, e.g.:
```ts
const createAdventureSchema = Joi.object({
name: Joi.string().max(100).required(),
description: Joi.string().max(255).allow('').optional(),
icon: Joi.string().max(100).optional(),
backgroundColor: Joi.string().max(50).optional(),
startDate: Joi.date().iso().optional(),
endDate: Joi.date().iso().min(Joi.ref('startDate')).optional(),
});

const createAdventureItemSchema = Joi.object({
name: Joi.string().max(150).required(),
amount: Joi.number().positive().precision(2).required(),
});
```

Services would follow the same pattern as `vaults.service.ts` / `vaults.repository.ts`
(`AdventuresService`, `AdventuresRepository`) with a computed `totalCost` field
summing `items.amount` for display as the "Quest Chest" total.

## 🗝️ Alternate Paths
- Could reuse Vaults with a "trip" flag instead of a new entity — rejected
because Adventures are bounded/one-off with a fixed item list, unlike ongoing
budget categories.
- Could store items as Tags on regular transactions — loses the dedicated
planning/estimate-vs-actual view.

## 💎 Treasures & Lore
- New nav item: **Adventures**
- Follows existing pixel-art UI conventions used by Vaults and Quests
- Frontend contract should live in `packages/shared/src/contracts/adventures.ts`,
mirroring `vaults.ts`

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.