From 3d4caeb9719913c2fc999cf066e8f079105c8f6c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:22:35 +0000 Subject: [PATCH] feat: update character list UI with vibe coded design - Update `CharacterService` to include generation counts (`_count.generations`) in the list response. - Update `CharacterAPI` to map `_count.generations` to a flat `looksCount` field. - Add new CSS classes (`clip-path-slant`, `clip-path-hex`, `scanline`, `font-display`) to `src/ui/index.css`. - Revamp `src/ui/pages/Characters.tsx` to use the new card design with color bars, looks count, and improved visuals. - Remove `src/ui/CharacterListDemo.tsx`. --- src/api/character.ts | 8 ++- src/service/character.ts | 3 + src/ui/CharacterListDemo.tsx | 99 ------------------------------- src/ui/index.css | 49 ++++++++++++++++ src/ui/pages/Characters.tsx | 109 +++++++++++++++++++++++++---------- 5 files changed, 139 insertions(+), 129 deletions(-) delete mode 100644 src/ui/CharacterListDemo.tsx diff --git a/src/api/character.ts b/src/api/character.ts index 2c7be2f..3306fae 100644 --- a/src/api/character.ts +++ b/src/api/character.ts @@ -26,7 +26,13 @@ const route = app .get('/', zValidator('query', listSchema), async (c) => { const { page, limit } = c.req.valid('query') const result = await characterService.listCharacters({ page, limit }) - return c.json(result) + return c.json({ + ...result, + items: result.items.map((item) => ({ + ...item, + looksCount: item._count.generations, + })), + }) }) .post('/', zValidator('json', createSchema), async (c) => { const { name, description, imageId } = c.req.valid('json') diff --git a/src/service/character.ts b/src/service/character.ts index 669e7ac..09c5ac0 100644 --- a/src/service/character.ts +++ b/src/service/character.ts @@ -24,6 +24,9 @@ export class CharacterService { take: limit, include: { image: true, + _count: { + select: { generations: true }, + }, }, orderBy: { createdAt: 'desc', diff --git a/src/ui/CharacterListDemo.tsx b/src/ui/CharacterListDemo.tsx deleted file mode 100644 index 3d86b6a..0000000 --- a/src/ui/CharacterListDemo.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Hexagon } from 'lucide-react' -import React from 'react' - -interface Character { - id: string - name: string - role: 'Vanguard' | 'Duelist' | 'Controller' | 'Sentinel' - difficulty: 'Low' | 'Medium' | 'High' - description: string - shortBio: string - imageUrl: string - themeColor: string // Hex code - accentColor: string // Hex code -} - -interface CharacterSelectorProps { - characters: Character[] - onSelect: (id: string) => void -} - -const CharacterSelector: React.FC = ({ characters, onSelect }) => { - // Hardcoded blue shadow for the outer glow - const blueShadow = 'rgba(59,130,246,0.6)' - - return ( -
-
-
- - System // Operator_Select -
-
-

- Select{' '} - - Operator - -

- - UNITS: {characters.length} - -
-
- -
- {characters.map((char, index) => ( -
onSelect(char.id)} - className="group relative h-64 cursor-pointer" - style={{ animation: `fadeIn 0.5s ease-out ${Math.min(index * 0.03, 0.5)}s backwards` }} - > - {/* Wrapper for the Drop Shadow (Outer Glow) */} -
- {/* 1. Base Border (Static Slate - Always visible as the thin border) */} -
- - {/* 2. Highlight Border (Blue - Fades in on hover, pre-scaled to avoid growth animation) */} - {/* Scale 1.035 is approx 70% of the previous 1.05 scale. */} -
- - {/* 3. Content Layer - Stays fixed size relative to parent, creating the cutout effect */} -
-
- {char.name} - {/* Gradient overlay for text readability */} -
-
- -
-
-
-

- {char.name} -

- - {char.role} - -
-
-
-
-
- ))} -
- -
- ) -} - -export default CharacterSelector diff --git a/src/ui/index.css b/src/ui/index.css index 1c1aa8c..9f158e6 100644 --- a/src/ui/index.css +++ b/src/ui/index.css @@ -80,6 +80,55 @@ body { animation: fade-in-up 0.4s ease-out forwards; } +/* Character List Demo Styles */ +.font-display { + font-family: + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + 'Noto Sans', + sans-serif; + font-weight: 800; +} +.clip-path-slant { + clip-path: polygon( + 20px 0, + 100% 0, + 100% calc(100% - 20px), + calc(100% - 20px) 100%, + 0 100%, + 0 20px + ); +} +.clip-path-hex { + clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); +} +@keyframes scanline { + 0% { + transform: translateY(-100%); + } + 100% { + transform: translateY(100%); + } +} +.scanline::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.03) 51%, transparent 51%); + background-size: 100% 4px; + pointer-events: none; + z-index: 50; +} + :root { --sidebar: hsl(0 0% 98%); --sidebar-foreground: hsl(240 5.3% 26.1%); diff --git a/src/ui/pages/Characters.tsx b/src/ui/pages/Characters.tsx index 2ee5d73..49872ed 100644 --- a/src/ui/pages/Characters.tsx +++ b/src/ui/pages/Characters.tsx @@ -1,15 +1,27 @@ import { InferResponseType } from 'hono/client' -import { Plus } from 'lucide-react' +import { Hexagon, Plus } from 'lucide-react' import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { client } from '../client' import { CreateCharacterDialog } from '../components/CreateCharacterDialog' -import { Avatar, AvatarFallback, AvatarImage } from '../components/ui/avatar' import { Button } from '../components/ui/button' type CharacterResponse = InferResponseType type Character = CharacterResponse['items'][number] +const getCharacterColor = (name: string) => { + const colors = [ + '#ef4444', // Red + '#f97316', // Orange + '#f59e0b', // Amber + '#22c55e', // Green + '#3b82f6', // Blue + '#a855f7', // Purple + ] + const charCode = name.charCodeAt(0) || 0 + return colors[charCode % colors.length] +} + export default function Characters() { const [characters, setCharacters] = useState([]) const [loading, setLoading] = useState(true) @@ -43,14 +55,28 @@ export default function Characters() { } return ( -
-
-

- Characters -

- +
+
+
+
+ + System // Operator_Select +
+

+ Select{' '} + + Operator + +

+
+
+ + UNITS: {characters.length} + + +
{characters.length === 0 ? ( @@ -58,29 +84,53 @@ export default function Characters() {

No characters found.

) : ( -
- {characters.map((char) => ( +
+ {characters.map((char, index) => (
navigate(`/characters/${char.id}/looks`)} + className="group relative h-64 cursor-pointer" + style={{ + animation: `fadeIn 0.5s ease-out ${Math.min(index * 0.03, 0.5)}s backwards`, + }} > -
- - - - {char.name.slice(0, 2).toUpperCase()} - - -
-

{char.name}

- {char.description && ( -

{char.description}

- )} + {/* Wrapper for the Drop Shadow (Outer Glow) */} +
+ {/* 1. Base Border (Static Slate - Always visible as the thin border) */} +
+ + {/* 2. Highlight Border (Blue - Fades in on hover, pre-scaled to avoid growth animation) */} + {/* Scale 1.035 is approx 70% of the previous 1.05 scale. */} +
+ + {/* 3. Content Layer - Stays fixed size relative to parent, creating the cutout effect */} +
+
+ {char.name} + {/* Gradient overlay for text readability */} +
+
+ +
+
+
+

+ {char.name} +

+ + {/* @ts-ignore */} + {char.looksCount} LOOKS + +
+
@@ -93,6 +143,7 @@ export default function Characters() { onOpenChange={setCreateOpen} onSuccess={fetchCharacters} /> +
) }