Merge branch 'main' of gitlab.com:soapbox-pub/ditto-mew
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { ExternalLink, Info } from 'lucide-react';
|
||||
|
||||
import type { ExtraKindDef } from '@/lib/extraKinds';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { ExternalFavicon } from '@/components/ExternalFavicon';
|
||||
|
||||
interface KindInfoButtonProps {
|
||||
kindDef: ExtraKindDef;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Info button that opens a modal with a blurb and external site links for an extra kind. */
|
||||
export function KindInfoButton({ kindDef, icon }: KindInfoButtonProps) {
|
||||
const { label, blurb, sites } = kindDef;
|
||||
|
||||
if (!sites?.length && !blurb) return null;
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="size-8 rounded-full text-muted-foreground hover:text-foreground">
|
||||
<Info className="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-w-xs p-6">
|
||||
<div className="flex flex-col items-center text-center gap-4">
|
||||
{icon && (
|
||||
<div className="text-primary [&>svg]:size-10">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogTitle className="text-lg">{label}</DialogTitle>
|
||||
|
||||
{blurb && (
|
||||
<DialogDescription className="text-sm leading-relaxed">
|
||||
{blurb}
|
||||
</DialogDescription>
|
||||
)}
|
||||
|
||||
{sites && sites.length > 0 && (
|
||||
<div className="w-full space-y-1.5 pt-1">
|
||||
{sites.map((site) => {
|
||||
const hostname = new URL(site.url).hostname;
|
||||
const name = site.name ?? hostname.split('.')[0].replace(/^./, (c) => c.toUpperCase());
|
||||
|
||||
return (
|
||||
<a
|
||||
key={site.url}
|
||||
href={site.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
<ExternalFavicon url={site.url} size={16} />
|
||||
{name}
|
||||
<ExternalLink className="size-3.5 opacity-70" />
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,14 @@ export interface SubKindDef {
|
||||
addressable: boolean;
|
||||
}
|
||||
|
||||
/** An external site where users can create or participate in a specific kind. */
|
||||
export interface ExtraKindSite {
|
||||
/** Full URL to the site. */
|
||||
url: string;
|
||||
/** Display name override. Defaults to the first segment of the hostname, capitalized. */
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/** Section labels for grouping extra kinds in settings UI. */
|
||||
export type ExtraKindSection = 'feed' | 'media' | 'social' | 'whimsy';
|
||||
|
||||
@@ -50,6 +58,10 @@ export interface ExtraKindDef {
|
||||
section: ExtraKindSection;
|
||||
/** If true, this entry only has a feed toggle (no sidebar toggle). */
|
||||
feedOnly?: boolean;
|
||||
/** Longer, whimsical blurb shown in the info modal. */
|
||||
blurb?: string;
|
||||
/** External sites where users can create or participate in this kind of content. */
|
||||
sites?: ExtraKindSite[];
|
||||
}
|
||||
|
||||
/** All supported extra content kinds, ordered by section (feed → media → social → whimsy). */
|
||||
@@ -91,6 +103,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
|
||||
route: 'articles',
|
||||
addressable: true,
|
||||
section: 'feed',
|
||||
blurb: 'Blog posts, essays, and guides. Write and publish from a dedicated editor.',
|
||||
sites: [{ url: 'https://inkwell.shakespeare.wtf' }],
|
||||
},
|
||||
// Media
|
||||
{
|
||||
@@ -102,6 +116,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
|
||||
route: 'vines',
|
||||
addressable: true,
|
||||
section: 'media',
|
||||
blurb: 'Short video clips. Record and share from a dedicated app.',
|
||||
sites: [{ url: 'https://divine.video' }],
|
||||
},
|
||||
{
|
||||
kind: 30311,
|
||||
@@ -112,6 +128,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
|
||||
route: 'streams',
|
||||
addressable: true,
|
||||
section: 'media',
|
||||
blurb: 'Watch and broadcast live video. Start a stream from a streaming app.',
|
||||
sites: [{ url: 'https://zap.stream', name: 'zap.stream' }],
|
||||
},
|
||||
// Social
|
||||
{
|
||||
@@ -123,6 +141,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
|
||||
route: 'polls',
|
||||
addressable: false,
|
||||
section: 'social',
|
||||
blurb: 'Ask a question, let people vote. Create polls from a polling app.',
|
||||
sites: [{ url: 'https://pollerama.fun' }],
|
||||
},
|
||||
{
|
||||
kind: 39089,
|
||||
@@ -133,6 +153,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
|
||||
route: 'packs',
|
||||
addressable: true,
|
||||
section: 'social',
|
||||
blurb: 'Curated lists of people to follow. Browse or create your own.',
|
||||
sites: [{ url: 'https://following.space', name: 'following.space' }, { url: 'https://following.party', name: 'following.party' }],
|
||||
},
|
||||
// Whimsy
|
||||
{
|
||||
@@ -144,6 +166,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
|
||||
route: 'colors',
|
||||
addressable: false,
|
||||
section: 'whimsy',
|
||||
blurb: 'Share your mood as a color palette. Pick colors that match the moment.',
|
||||
sites: [{ url: 'https://espy.you' }],
|
||||
},
|
||||
{
|
||||
kind: 37381,
|
||||
@@ -154,6 +178,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
|
||||
route: 'decks',
|
||||
addressable: true,
|
||||
section: 'whimsy',
|
||||
blurb: 'Magic: The Gathering deck lists. Build and share decks with other players.',
|
||||
sites: [{ url: 'https://surveil.cards' }],
|
||||
},
|
||||
{
|
||||
kind: 37516,
|
||||
@@ -163,6 +189,8 @@ export const EXTRA_KINDS: ExtraKindDef[] = [
|
||||
route: 'treasures',
|
||||
addressable: true,
|
||||
section: 'whimsy',
|
||||
blurb: 'Real-world geocaching. Hide treasures outside, find others, and log your discoveries.',
|
||||
sites: [{ url: 'https://treasures.to' }],
|
||||
subKinds: [
|
||||
{
|
||||
kind: 37516,
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Feed } from '@/components/Feed';
|
||||
import { KindInfoButton } from '@/components/KindInfoButton';
|
||||
import { useLayoutOptions } from '@/contexts/LayoutContext';
|
||||
import { EXTRA_KINDS, type ExtraKindDef } from '@/lib/extraKinds';
|
||||
|
||||
interface KindFeedPageProps {
|
||||
kind: number | number[];
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
emptyMessage?: string;
|
||||
/** Override the auto-detected ExtraKindDef (useful for pages with sub-kinds like Treasures). */
|
||||
kindDef?: ExtraKindDef;
|
||||
}
|
||||
|
||||
export function KindFeedPage({ kind, title, icon, emptyMessage }: KindFeedPageProps) {
|
||||
export function KindFeedPage({ kind, title, icon, emptyMessage, kindDef }: KindFeedPageProps) {
|
||||
const primaryKind = Array.isArray(kind) ? kind[0] : kind;
|
||||
|
||||
const resolvedDef = useMemo(
|
||||
() => kindDef ?? EXTRA_KINDS.find((def) => def.kind === primaryKind),
|
||||
[kindDef, primaryKind],
|
||||
);
|
||||
|
||||
useSeoMeta({
|
||||
title: `${title} | Ditto`,
|
||||
description: `${title} on Nostr`,
|
||||
@@ -33,10 +43,11 @@ export function KindFeedPage({ kind, title, icon, emptyMessage }: KindFeedPagePr
|
||||
<Link to="/" className="p-2 -ml-2 rounded-full hover:bg-secondary transition-colors sidebar:hidden">
|
||||
<ArrowLeft className="size-5" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{icon}
|
||||
<h1 className="text-xl font-bold">{title}</h1>
|
||||
</div>
|
||||
{resolvedDef && <KindInfoButton kindDef={resolvedDef} icon={icon} />}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -9,15 +9,19 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { KindInfoButton } from '@/components/KindInfoButton';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useStreamKind } from '@/hooks/useStreamKind';
|
||||
import { useLayoutOptions } from '@/contexts/LayoutContext';
|
||||
import { getDisplayName } from '@/lib/getDisplayName';
|
||||
import { useProfileUrl } from '@/hooks/useProfileUrl';
|
||||
import { useOpenPost } from '@/hooks/useOpenPost';
|
||||
import { EXTRA_KINDS } from '@/lib/extraKinds';
|
||||
import { timeAgo } from '@/lib/timeAgo';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const streamsDef = EXTRA_KINDS.find((def) => def.route === 'streams')!;
|
||||
|
||||
/** Extract the first value of a tag by name. */
|
||||
function getTag(tags: string[][], name: string): string | undefined {
|
||||
return tags.find(([n]) => n === name)?.[1];
|
||||
@@ -66,10 +70,11 @@ export function StreamsFeedPage() {
|
||||
<Link to="/" className="p-2 -ml-2 rounded-full hover:bg-secondary transition-colors sidebar:hidden">
|
||||
<ArrowLeft className="size-5" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<Radio className="size-5" />
|
||||
<h1 className="text-xl font-bold">Streams</h1>
|
||||
</div>
|
||||
<KindInfoButton kindDef={streamsDef} icon={<Radio className="size-5" />} />
|
||||
</div>
|
||||
|
||||
{/* Feed */}
|
||||
|
||||
@@ -15,6 +15,7 @@ export function TreasuresPage() {
|
||||
kind={kinds}
|
||||
title="Treasures"
|
||||
icon={<ChestIcon className="size-5" />}
|
||||
kindDef={treasuresDef}
|
||||
emptyMessage={
|
||||
kinds.length === 0
|
||||
? 'All treasure types are disabled. Enable geocaches or found logs in Settings > Feed.'
|
||||
|
||||
Reference in New Issue
Block a user