diff --git a/README.md b/README.md index 0065fd8..e9efc47 100644 --- a/README.md +++ b/README.md @@ -329,109 +329,7 @@ The items below are planned improvements roughly ordered by user value. They are --- -### 1. Username Display Names & Password Changes on Profile - -**Problem:** Usernames are forced lowercase on registration (some users want "JohnDoe" not "johndoe"). Users also have no self-service way to change their password. - -**Plan:** -- Add a `displayUsername` field to `User` — stores the original mixed-case version typed at signup. Lookups/URLs continue using the lowercase `username` for uniqueness and consistency. -- Profile settings page (`/profile/[username]/settings` or a tab on the existing profile) with: - - **Change display name** — updates `displayUsername` only; the canonical `username` remains lowercase and immutable (avoids breaking existing links/positions). - - **Change username** — updates both `username` and `displayUsername`; requires confirming no conflicts; all foreign key relations in Prisma use the DB `id` so no cascade issues. - - **Change password** — classic current-password + new-password + confirm form; server endpoint `PATCH /api/user/me` validating current hash before updating. -- All places that show a username (Navbar, profile page, leaderboard, trade history) should render `displayUsername` if set, falling back to `username`. - ---- - -### 2. Leaderboard - -**Problem:** No public way to see who the top players are or browse other users' portfolios. - -**Plan:** -- New page `/leaderboard` showing top N players sorted by **net worth** (balance + sum of position value at current prices). -- Table columns: rank, avatar/username link → their public profile, net worth, total trades, biggest single position. -- Public profiles — any user can view `/profile/[username]`; sensitive data (exact balance) optionally hidden from non-owners/non-admins. -- Navbar: show **Leaderboard** link when signed in (replacing or supplementing the current unauthenticated-only links). -- Consider a separate "all-time best trade" leaderboard tab for engagement. - ---- - -### 3. Lucky Dip Lottery - -**Problem:** Players who go broke have no recovery mechanic; there is no daily engagement hook beyond trading. - -**Plan:** -- New page `/lottery`. -- Once per day (tracked by a `lastLotteryAt` timestamp on `User`), a player who has **$0 or less** can reveal one hidden box from a grid (e.g. 5 × 5 = 25 boxes). -- One box is the winner (selected server-side at draw time, stored encrypted or revealed only on pick to prevent client-side cheating). -- **Prizes** (configurable): e.g. one box pays $100, a few pay $10, the rest pay $0. -- API: `POST /api/lottery/pick` — validates cooldown, selects winner server-side, awards balance, returns outcome. -- Consider allowing players with any balance to play for a small stake (e.g. pay $5 to play, win up to $200) to make it useful beyond the broke scenario. -- Future: admin-configurable prize pool and grid size. - ---- - -### 4. Home Page Nav Changes (Signed-In vs Signed-Out) - -**Problem:** Home page shows the same call-to-action links regardless of auth state. - -**Plan:** -- When **signed out**: show Sign Up / Sign In CTAs as currently. -- When **signed in**: replace or supplement with quick links to **Leaderboard**, **Lottery**, and the user's own profile. -- Navbar already hides/shows some items; extend that logic to the hero section of the home page. - ---- - -### 5. Related Hashtags - -**Problem:** No discovery path from one hashtag to related ones. - -**Plan:** -- During price update jobs the worker already fetches up to 200 posts. Parse each post's `tags` array (present in the Mastodon API response) to collect co-occurring hashtags. -- Store co-occurrence counts in a new `RelatedHashtag` table: `(hashtagId, relatedTag, coOccurrences)` — upsert on each price update, incrementing counts. -- On the hashtag detail page, show the top 5 most co-occurring tags as "Related" chips that link to their own pages. -- Only surface related tags that are themselves active (or researchable) — avoid surfacing banned/inactive noise. -- Worker change: `MastodonPost` type already has `content`; add `tags: { name: string }[]` to the interface and parse it in `mastodon.ts`. - ---- - -### 6. Admin: Price Audit Log - -**Problem:** Admins have no visibility into the math behind a given price update. - -**Plan:** -- The `PriceHistory` table already stores `postsPerHour` alongside `price`. Surface this in two places: - 1. **Hashtag detail page** — show the most recent `postsPerHour` reading and the formula result as a tooltip or sub-line under the current price (e.g. _"$1.49 — 5.96 posts/hr at last update"_). - 2. **Admin stocks page** — add a "History" drawer/modal per hashtag showing the last N `PriceHistory` rows as a table: `recordedAt | postsPerHour | price | Δ price` so admins can see the full audit trail. -- No schema changes needed — `postsPerHour` is already stored. - ---- - -### 7. Admin: Add Hashtag Manually - -**Problem:** Admins cannot bypass the research-point system to seed the exchange with interesting hashtags. - -**Plan:** -- "Add hashtag" button on `/admin/stocks` that opens a modal with a tag input. -- API: `POST /api/admin/stocks` — normalizes tag, queries Mastodon immediately (same as research flow), upserts hashtag, queues an initial price-update job. Returns error if Mastodon returns nothing. -- Optionally allow admin to force-add with a manual starting price even if Mastodon has no results (useful for bootstrapping/testing). - ---- - -### 8. Admin: Ban / Block Hashtags - -**Problem:** No way to prevent abusive or NSFW hashtags from being researched into the exchange. - -**Plan:** -- Add `isBanned: Boolean @default(false)` field to the `Hashtag` model. -- Admin UI: "Ban" button on the stocks page that sets `isBanned = true` and `isActive = false`. Banned hashtags still exist in the DB for record-keeping but are not researchable. -- Research API (`/api/research`): check `isBanned` on any existing matching hashtag — if banned return a `403` with a user-friendly message (e.g. _"This hashtag is not available on HashEx."_) without spending a research point. -- New pre-ban blocklist: a static list (or admin-editable table) of tags that are auto-banned on first research attempt, before any Mastodon query. -- Banned tags should still appear in the admin stocks list with a distinct visual indicator and an "Unban" toggle. - ---- - -### 9. Auto-Derive Display Casing from Mastodon Posts +### 1. Auto-Derive Display Casing from Mastodon Posts **Problem:** `displayTag` is currently set once at research/creation time from whatever the user typed. In reality Mastodon users have an established "canonical" capitalisation for a tag (e.g. `#StPatricksDay` rather than `#stpatricksday`) and our display tag should reflect that. @@ -445,6 +343,45 @@ The items below are planned improvements roughly ordered by user value. They are --- +### 2. Home Page Holdings Summary (Signed-In) + +**Problem:** Signed-in users land on the home page and see only the generic trending list — there is no personalised hook to show how their portfolio is performing. + +**Plan:** +- Add two summary cards above the "Trending now" section, visible only to signed-in users: **Biggest Gain** and **Biggest Loss** (by unrealised P&L across open positions). +- Both cards link to the relevant hashtag page. +- Implemented as a server component addition to `src/app/page.tsx`: query the current user's open positions (joined with current price) and compute unrealised P&L per position to find the top and bottom. +- If the user has no positions, the cards are omitted (no empty-state clutter). +- No schema changes required. + +--- + +### 3. Search Autocomplete + +**Problem:** The current search/research input is a plain text field with no discovery aid — users must know or guess full hashtag names. + +**Plan:** +- While the user types in the search box, show a dropdown of matching hashtags that are **already tracked** on the exchange (fetched from a new `GET /api/hashtags/search?q=` endpoint). +- The autocomplete only surfaces existing active hashtags; submitting a tag that doesn't appear in the list still proceeds through the normal research flow (no interference with new-hashtag discovery). +- Client-side: debounce the input (~300 ms), cancel in-flight requests on new keystrokes, close dropdown on `Escape` or outside click. +- Endpoint: case-insensitive prefix match on `Hashtag.tag` where `isActive = true` and `isBanned = false`, returning `{ tag, displayTag, currentPrice }` for up to 8 results. +- No schema changes required. + +--- + +### 4. Open Positions Full Page + +**Problem:** The profile page shows open positions in a compact list — there is no space for richer per-position data (cost basis, current value, a mini P&L chart). + +**Plan:** +- New page `/positions` (auth-protected) showing only the signed-in user's open positions in a more detailed layout. +- Each row / card: hashtag name + link, position type (LONG/SHORT), shares held, average buy price, current price, total cost basis, current value, unrealised P&L and P&L %, a sparkline chart of the hashtag's recent price history. +- Link into this page from the "Open positions" heading on the profile page. +- No new nav item needed — accessed via profile page link. +- No schema changes required. + +--- + ### Other Ideas / Nice-to-Haves - **Hedge funds**: group of players pool money into a shared portfolio, one designated fund manager places trades. diff --git a/src/app/lottery/page.tsx b/src/app/lottery/page.tsx index 9434426..7c6a118 100644 --- a/src/app/lottery/page.tsx +++ b/src/app/lottery/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState } from 'react' -import { Loader2, Ticket, CheckCircle2 } from 'lucide-react' +import { Loader2, Dices, CheckCircle2 } from 'lucide-react' import { formatCurrency } from '@/lib/utils' import { useSession } from 'next-auth/react' import Link from 'next/link' @@ -66,7 +66,7 @@ export default function LotteryPage() { if (!session) { return (
- +

Sign in to play the Lucky Dip.

- +

Lucky Dip

diff --git a/src/app/profile/[username]/page.tsx b/src/app/profile/[username]/page.tsx index 022b512..a2b103e 100644 --- a/src/app/profile/[username]/page.tsx +++ b/src/app/profile/[username]/page.tsx @@ -101,17 +101,6 @@ export default async function ProfilePage({ params }: Props) { />

- {/* Account settings — only shown to the profile owner */} - {isOwn && ( - <> - - - - )} - {/* Positions */} {user.positions.length > 0 && (
@@ -203,6 +192,17 @@ export default async function ProfilePage({ params }: Props) {
)} + + {/* Account settings — only shown to the profile owner */} + {isOwn && ( + <> + + + + )} ) }