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.

Aberta
#244 6 comentários 0 reações 1 responsável Reivindicada por @rahulkatiayr Ver no GitHub
backend frontend up for grabs
Linguagem predominante
TypeScript
Estrelas
14
Forks
91
Métricas de merge de PRs
Nenhum PR com merge em 30d

Descrição

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`

Guia de contribuição

Nenhum guia de contribuição indexado para este repositório

Direção de pesquisa

Start by reading Vault.entity.ts and the vaults.routes.ts, vaults.service.ts, and vaults.repository.ts patterns, then review packages/shared/src/contracts/vaults.ts and the existing Vaults and Quests UI conventions. Done means the requested Adventure and AdventureItem models, routes, validation, shared contract, navigation entry, and planning/completion UI are integrated consistently across the application.

Escrita pelo modelo de indexação a partir do texto da issue.

Avaliação

Stack de tecnologia
nextjs, nodejs, react, typescript
Domínio
backend-api-design, databases, frontend, full-stack
Tipo de issue
Funcionalidade
Dificuldade
5/5
Tempo estimado
Mais de uma semana
Status de atividade
Pouca atividade
Clareza
Razoavelmente clara
Facilidade para iniciantes
25/100

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.