From dbb044ced400dfc7b2e63694468be201a6d2ea46 Mon Sep 17 00:00:00 2001 From: Desmond Edem Date: Sun, 17 May 2026 09:26:24 +0100 Subject: [PATCH] feat: polish community gallery --- apps/web/src/components/Gallery.tsx | 215 +++++++++++++++++++------- apps/web/src/components/MiniDots.tsx | 60 +++++++ apps/web/src/components/Templates.tsx | 55 +------ apps/web/src/pages/GalleryPage.tsx | 11 ++ 4 files changed, 234 insertions(+), 107 deletions(-) create mode 100644 apps/web/src/components/MiniDots.tsx diff --git a/apps/web/src/components/Gallery.tsx b/apps/web/src/components/Gallery.tsx index bfbb29c..aacedc2 100644 --- a/apps/web/src/components/Gallery.tsx +++ b/apps/web/src/components/Gallery.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { ApiError, fetchGallery } from '../lib/api' import { FatalError } from './FatalError' +import { MiniDots } from './MiniDots' import type { GallerySort, GallerySummary } from '../lib/api.types' const colors = { @@ -34,6 +35,29 @@ function formatDate(iso: string): string { return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) } +// Visually distinct thumbnail accents. Six colours already used elsewhere in +// the design system; no new colours introduced for this. +const GALLERY_COLOURS = [ + '#00e5ff', // cyan + '#00e676', // green + '#ffab00', // amber + '#d500f9', // magenta + '#ff5252', // coral + '#ffd600', // yellow +] as const + +// Simple FNV-1a-style hash; deterministic so the same slug always picks the +// same palette index. Stable across reloads and across the config's position +// in the result list. +function slugToColour(slug: string): string { + let hash = 2166136261 + for (let i = 0; i < slug.length; i++) { + hash ^= slug.charCodeAt(i) + hash = Math.imul(hash, 16777619) + } + return GALLERY_COLOURS[Math.abs(hash) % GALLERY_COLOURS.length] +} + export const Gallery: React.FC = () => { const navigate = useNavigate() @@ -302,7 +326,7 @@ const GalleryCard: React.FC<{ onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ - padding: 16, + padding: 0, background: colors.cardBackground, border: `1px solid ${hovered ? colors.borderHover : colors.border}`, borderRadius: 8, @@ -311,68 +335,153 @@ const GalleryCard: React.FC<{ cursor: 'pointer', display: 'flex', flexDirection: 'column', - gap: 8, + overflow: 'hidden', }} >
- {item.title} + +
-
- {item.author && @{item.author.username}} - - {item.viewCount} view{item.viewCount !== 1 ? 's' : ''} - - - {item.forkCount} fork{item.forkCount !== 1 ? 's' : ''} - - {formatDate(item.createdAt)} -
+
+
+ {item.title} +
- {item.tags.length > 0 && ( -
- {item.tags.map((t) => { - const isActive = activeTag === t - return ( - { - e.stopPropagation() - onTagClick(t) - }} - style={{ - padding: '2px 8px', - background: isActive ? `${colors.primary}20` : 'rgba(0, 229, 255, 0.06)', - border: `1px solid ${isActive ? colors.primary : colors.border}`, - borderRadius: 10, - color: isActive ? colors.primary : colors.textSecondary, - fontSize: 9, - cursor: 'pointer', - }} - > - {t} - - ) - })} +
+ {item.author && } + {item.author && @{item.author.username}} + {formatDate(item.createdAt)}
- )} + + {item.tags.length > 0 && ( +
+ {item.tags.map((t) => { + const isActive = activeTag === t + return ( + { + e.stopPropagation() + onTagClick(t) + }} + style={{ + padding: '2px 8px', + background: isActive ? `${colors.primary}20` : 'rgba(0, 229, 255, 0.06)', + border: `1px solid ${isActive ? colors.primary : colors.border}`, + borderRadius: 10, + color: isActive ? colors.primary : colors.textSecondary, + fontSize: 9, + cursor: 'pointer', + }} + > + {t} + + ) + })} +
+ )} +
) } + +const CounterOverlay: React.FC<{ + views: number + forks: number +}> = ({ views, forks }) => ( +
+ + +
+) + +const CounterChip: React.FC<{ + icon: 'eye' | 'fork' + value: number +}> = ({ icon, value }) => ( + + {icon === 'eye' ? ( + + + + ) : ( + + + + )} + {value} + +) + +const AuthorAvatar: React.FC<{ username: string }> = ({ username }) => ( +
+ {username.slice(0, 2).toUpperCase()} +
+) diff --git a/apps/web/src/components/MiniDots.tsx b/apps/web/src/components/MiniDots.tsx new file mode 100644 index 0000000..95c33e5 --- /dev/null +++ b/apps/web/src/components/MiniDots.tsx @@ -0,0 +1,60 @@ +import React from 'react' + +/** + * Generic preview glyph used in card thumbnails. Same topology for every + * config — only the stroke colour of the top row changes. The bottom row is + * always green to evoke "services running." + * + * Per-card visual variety is deferred to a future phase that can render real + * previews from each config's YAML. + */ +export const MiniDots: React.FC<{ color: string }> = ({ color }) => ( + + + + + {[30, 80, 130, 170].map((x, i) => ( + + + + + ))} + {[40, 80, 120, 160].map((x, i) => ( + + ))} + +) diff --git a/apps/web/src/components/Templates.tsx b/apps/web/src/components/Templates.tsx index 9d40ecc..54dbc60 100644 --- a/apps/web/src/components/Templates.tsx +++ b/apps/web/src/components/Templates.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react' import { useNavigate } from 'react-router-dom' import { ApiError, fetchTemplate, fetchTemplates, createTemplateFromSlug } from '../lib/api' import { FatalError } from './FatalError' +import { MiniDots } from './MiniDots' import { useAuth } from '../context/AuthContext' import type { TemplateCategory, TemplateSummary } from '../lib/api.types' @@ -52,60 +53,6 @@ const CATEGORY_OPTIONS: CategoryOption[] = [ { value: 'home-automation', label: 'HOME AUTOMATION' }, ] -// Generic preview glyph. Same topology for every template — only the stroke -// colour changes. Per-template visual variety is deferred to a future phase -// that can render real previews from the YAML. -const MiniDots: React.FC<{ color: string }> = ({ color }) => ( - - - - - {[30, 80, 130, 170].map((x, i) => ( - - - - - ))} - {[40, 80, 120, 160].map((x, i) => ( - - ))} - -) - export const Templates: React.FC = () => { const navigate = useNavigate() const { isLoggedIn } = useAuth() diff --git a/apps/web/src/pages/GalleryPage.tsx b/apps/web/src/pages/GalleryPage.tsx index a2dad36..dbffa67 100644 --- a/apps/web/src/pages/GalleryPage.tsx +++ b/apps/web/src/pages/GalleryPage.tsx @@ -8,6 +8,7 @@ const colors = { border: 'rgba(0, 229, 255, 0.12)', textPrimary: '#e0f7fa', textSecondary: '#78909c', + textMuted: '#455a64', } const fonts = { @@ -68,6 +69,16 @@ export const GalleryPage: React.FC = () => { > GALLERY + + // what the community is running +