05rgfly6g: Add HexAvatar and use it for every existing avatar
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { HexAvatar } from "@/components/HexAvatar";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { DiscoverCategory } from "@/lib/discover";
|
||||
import { fetchDiscover } from "@/lib/discover-client";
|
||||
import type { ExpertResult } from "@/lib/experts-search";
|
||||
import { fetchExperts } from "@/lib/experts-search-client";
|
||||
import { initials } from "@/lib/feed-model";
|
||||
|
||||
export type SearchStatus = "idle" | "loading" | "ready" | "error";
|
||||
|
||||
@@ -61,12 +61,7 @@ export function ExpertSearchView(p: ExpertSearchViewProps) {
|
||||
<ul className="rail-list">
|
||||
{p.results.map((e) => (
|
||||
<li key={String(e.id)} className="rail-card expert-card" data-testid="search-result">
|
||||
{e.pictureUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="avatar" src={e.pictureUrl} alt="" width={40} height={40} />
|
||||
) : (
|
||||
<span className="avatar avatar-initials" aria-hidden="true">{initials(e.displayName)}</span>
|
||||
)}
|
||||
<HexAvatar name={e.displayName} pictureUrl={e.pictureUrl} size="md" />
|
||||
<div className="rail-text">
|
||||
<strong className="rail-name">{e.displayName || "Expert"}</strong>
|
||||
{e.byline ? <span className="rail-byline">{e.byline}</span> : null}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HexAvatar } from "@/components/HexAvatar";
|
||||
import type { DiscoverExpert } from "@/lib/discover";
|
||||
import { initials } from "@/lib/feed-model";
|
||||
|
||||
function Rating({ rating, reviews }: { rating: number | null; reviews: number | null }) {
|
||||
if (rating === null && reviews === null) return null;
|
||||
@@ -19,12 +19,7 @@ export function FeaturedExpertsRail({ experts }: { experts: DiscoverExpert[] })
|
||||
<ul className="rail-list">
|
||||
{experts.map((e) => (
|
||||
<li key={String(e.id)} className="rail-card expert-card" data-testid="expert-card">
|
||||
{e.pictureUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="avatar" src={e.pictureUrl} alt="" width={40} height={40} />
|
||||
) : (
|
||||
<span className="avatar avatar-initials" aria-hidden="true">{initials(e.displayName)}</span>
|
||||
)}
|
||||
<HexAvatar name={e.displayName} pictureUrl={e.pictureUrl} size="md" />
|
||||
<div className="rail-text">
|
||||
<strong className="rail-name">{e.displayName || "Expert"}</strong>
|
||||
{e.byline ? <span className="rail-byline">{e.byline}</span> : null}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { initials } from "@/lib/feed-model";
|
||||
|
||||
export type HexAvatarSize = "lg" | "md" | "sm";
|
||||
|
||||
/** Width by height in px for each spec size (ratio 0.866). */
|
||||
export const HEX_SIZES: Record<HexAvatarSize, { width: number; height: number }> = {
|
||||
lg: { width: 46, height: 53 },
|
||||
md: { width: 38, height: 44 },
|
||||
sm: { width: 30, height: 35 },
|
||||
};
|
||||
|
||||
export const HEX_TONES = ["oxblood", "navy", "forest", "slate", "plum", "graphite"] as const;
|
||||
export type HexTone = (typeof HEX_TONES)[number];
|
||||
|
||||
/** Same name always gives the same tone. */
|
||||
export function toneFor(name: string): HexTone {
|
||||
let sum = 0;
|
||||
for (const ch of name.trim().toLowerCase()) sum = (sum * 31 + ch.codePointAt(0)!) >>> 0;
|
||||
return HEX_TONES[sum % HEX_TONES.length];
|
||||
}
|
||||
|
||||
export interface HexAvatarProps {
|
||||
name: string;
|
||||
pictureUrl?: string | null;
|
||||
size?: HexAvatarSize;
|
||||
}
|
||||
|
||||
export function HexAvatar({ name, pictureUrl, size = "md" }: HexAvatarProps) {
|
||||
const { width, height } = HEX_SIZES[size];
|
||||
if (pictureUrl) {
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className={`hex-avatar hex-avatar-${size}`} src={pictureUrl} alt="" width={width} height={height} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className={`hex-avatar hex-avatar-${size} hex-avatar-initials hex-avatar-${toneFor(name)}`} aria-hidden="true">
|
||||
{initials(name)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { HexAvatar } from "@/components/HexAvatar";
|
||||
import { CommentsSection } from "@/components/CommentsSection";
|
||||
import { VideoPlayer } from "@/components/VideoPlayer";
|
||||
import { initials, relativeTime, type CardPost } from "@/lib/feed-model";
|
||||
import { relativeTime, type CardPost } from "@/lib/feed-model";
|
||||
|
||||
export interface PostCardProps {
|
||||
post: CardPost;
|
||||
@@ -36,14 +37,7 @@ export function PostCard({ post, now, onToggleLike, likeBusy, likeError, onSigne
|
||||
return (
|
||||
<article className="post" data-testid="post-card">
|
||||
<header className="post-head">
|
||||
{post.authorPictureUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="avatar" src={post.authorPictureUrl} alt="" width={40} height={40} />
|
||||
) : (
|
||||
<span className="avatar avatar-initials" aria-hidden="true">
|
||||
{initials(post.authorName)}
|
||||
</span>
|
||||
)}
|
||||
<HexAvatar name={post.authorName} pictureUrl={post.authorPictureUrl} size="lg" />
|
||||
<div className="post-who">
|
||||
<strong>{post.authorName}</strong>
|
||||
<time dateTime={post.publishedAt ?? undefined}>{relativeTime(post.publishedAt, now)}</time>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HexAvatar } from "@/components/HexAvatar";
|
||||
import type { DiscoverMinnect } from "@/lib/discover";
|
||||
import { MinnectMedia } from "./MinnectMedia";
|
||||
import { initials } from "@/lib/feed-model";
|
||||
|
||||
export function TrendingMinnectsRail({ minnects }: { minnects: DiscoverMinnect[] }) {
|
||||
if (minnects.length === 0) return null;
|
||||
@@ -13,12 +13,7 @@ export function TrendingMinnectsRail({ minnects }: { minnects: DiscoverMinnect[]
|
||||
return (
|
||||
<li key={String(m.id)} className="rail-card minnect-card" data-testid="minnect-card">
|
||||
<div className="minnect-who">
|
||||
{m.expert.pictureUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="avatar avatar-sm" src={m.expert.pictureUrl} alt="" width={28} height={28} />
|
||||
) : (
|
||||
<span className="avatar avatar-sm avatar-initials" aria-hidden="true">{initials(who)}</span>
|
||||
)}
|
||||
<HexAvatar name={who} pictureUrl={m.expert.pictureUrl} size="sm" />
|
||||
<strong className="rail-name">{who}</strong>
|
||||
</div>
|
||||
<MinnectMedia media={m.media} thumbnailUrl={m.thumbnailUrl} question={m.question} who={who} />
|
||||
|
||||
Reference in New Issue
Block a user