feat(factions): redesign faction card and add faction detail page
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
ab0ff6b96e
commit
110112e9b4
2 changed files with 540 additions and 43 deletions
464
src/app/(frontend)/intelligence/factions/[id]/page.tsx
Normal file
464
src/app/(frontend)/intelligence/factions/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
import config from "@payload-config";
|
||||
import { getPayload } from "payload";
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import type { Faction, Media, Mission, BankAccount, Resource, Campaign } from "@/payload-types";
|
||||
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||
import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ChevronRightIcon,
|
||||
FlagIcon,
|
||||
LandmarkIcon,
|
||||
ShieldIcon,
|
||||
ScaleIcon,
|
||||
} from "lucide-react";
|
||||
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||||
import { formatAmount } from "@/lib/banking/format";
|
||||
|
||||
interface FactionDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
playerFaction: "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30",
|
||||
majorFaction: "bg-blue-500/20 text-blue-400",
|
||||
minorFaction: "bg-muted text-muted-foreground",
|
||||
corporation: "bg-amber-500/20 text-amber-400",
|
||||
special: "bg-purple-500/20 text-purple-400",
|
||||
};
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
playerFaction: "Player Faction",
|
||||
majorFaction: "Major Faction",
|
||||
minorFaction: "Minor Faction",
|
||||
corporation: "Corporation",
|
||||
special: "Special",
|
||||
};
|
||||
|
||||
const missionStatusColors: Record<string, string> = {
|
||||
Concept: "bg-muted text-muted-foreground",
|
||||
Planning: "bg-muted text-muted-foreground",
|
||||
Ready: "bg-blue-500/20 text-blue-400",
|
||||
Scheduled: "bg-amber-500/20 text-amber-400",
|
||||
Active: "bg-green-500/20 text-green-400",
|
||||
Completed: "bg-emerald-500/20 text-emerald-400",
|
||||
Cancelled: "bg-red-500/20 text-red-400",
|
||||
};
|
||||
|
||||
const campaignStatusColors: Record<string, string> = {
|
||||
concept: "bg-muted text-muted-foreground",
|
||||
inProgress: "bg-blue-500/20 text-blue-400",
|
||||
completed: "bg-emerald-500/20 text-emerald-400",
|
||||
};
|
||||
|
||||
function reputationTone(rep: number): string {
|
||||
if (rep >= 50) return "bg-emerald-500/20 text-emerald-400";
|
||||
if (rep >= 20) return "bg-green-500/20 text-green-400";
|
||||
if (rep > -20) return "bg-muted text-muted-foreground";
|
||||
if (rep > -50) return "bg-amber-500/20 text-amber-400";
|
||||
return "bg-red-500/20 text-red-400";
|
||||
}
|
||||
|
||||
function reputationLabel(rep: number): string {
|
||||
if (rep >= 50) return "Revered";
|
||||
if (rep >= 20) return "Respected";
|
||||
if (rep > -20) return "Neutral";
|
||||
if (rep > -50) return "Disliked";
|
||||
return "Reviled";
|
||||
}
|
||||
|
||||
function relationshipTone(value: number): { bar: string; text: string; label: string } {
|
||||
// 50..100 allied (green), 20..49 friendly (green-soft), -19..20 neutral, -20..-49 tense, -50..-100 hostile
|
||||
if (value >= 50) return { bar: "bg-emerald-500", text: "text-emerald-400", label: "Allied" };
|
||||
if (value >= 20) return { bar: "bg-green-500", text: "text-green-400", label: "Friendly" };
|
||||
if (value > -20)
|
||||
return { bar: "bg-muted-foreground", text: "text-muted-foreground", label: "Neutral" };
|
||||
if (value > -50) return { bar: "bg-amber-500", text: "text-amber-400", label: "Tense" };
|
||||
return { bar: "bg-red-500", text: "text-red-400", label: "Hostile" };
|
||||
}
|
||||
|
||||
export default async function FactionDetailPage({ params }: FactionDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const payload = await getPayload({ config });
|
||||
|
||||
const faction = await payload
|
||||
.findByID({
|
||||
collection: "factions",
|
||||
id: Number(id),
|
||||
depth: 2,
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
if (!faction) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const coverImage =
|
||||
typeof faction.coverImage === "object" && faction.coverImage !== null
|
||||
? (faction.coverImage as Media)
|
||||
: null;
|
||||
const coverImageUrl = coverImage?.url ?? null;
|
||||
|
||||
const descriptionText = faction.description
|
||||
? convertLexicalToPlaintext({ data: faction.description })
|
||||
: null;
|
||||
const heroSubtitle =
|
||||
descriptionText && descriptionText.length > 240
|
||||
? descriptionText.slice(0, 240) + "…"
|
||||
: (descriptionText ?? faction.name);
|
||||
|
||||
const reputation = faction.reputation ?? 0;
|
||||
|
||||
// Bank account for this faction
|
||||
const bankRes = await payload
|
||||
.find({
|
||||
collection: "bank-accounts",
|
||||
where: {
|
||||
and: [{ ownerFaction: { equals: faction.id } }, { accountType: { equals: "faction" } }],
|
||||
},
|
||||
depth: 1,
|
||||
limit: 1,
|
||||
})
|
||||
.catch(() => ({ docs: [] }));
|
||||
const bankAccount = (bankRes.docs[0] as BankAccount | undefined) ?? null;
|
||||
const bankCurrency =
|
||||
bankAccount && typeof bankAccount.currency === "object"
|
||||
? (bankAccount.currency as Resource)
|
||||
: null;
|
||||
|
||||
// Missions involving this faction
|
||||
const missionsRes = await payload
|
||||
.find({
|
||||
collection: "missions",
|
||||
where: { "factionsInvolved.faction": { equals: faction.id } },
|
||||
sort: "-classification.startDateTime",
|
||||
limit: 100,
|
||||
depth: 0,
|
||||
select: {
|
||||
name: true,
|
||||
codeName: true,
|
||||
summary: true,
|
||||
ownershipAndStatus: true,
|
||||
classification: true,
|
||||
},
|
||||
})
|
||||
.catch(() => ({ docs: [] as Mission[] }));
|
||||
const missions = missionsRes.docs as Mission[];
|
||||
|
||||
// Associated campaigns (faction.associatedCampaigns is populated at depth 2)
|
||||
const campaigns =
|
||||
faction.associatedCampaigns?.filter(
|
||||
(c): c is Campaign => typeof c === "object" && c !== null,
|
||||
) ?? [];
|
||||
|
||||
// Relationships (targetFaction populated at depth 2)
|
||||
const relationships = (faction.relationships ?? []).filter(
|
||||
(r) => typeof r.targetFaction === "object" && r.targetFaction !== null,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<header className="relative min-h-[500px] w-full overflow-hidden bg-background">
|
||||
<div className="absolute top-0 left-0 px-5 pt-5 pb-3 z-10">
|
||||
<Link
|
||||
href="/intelligence/factions"
|
||||
className="flex w-fit items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary"
|
||||
>
|
||||
<ArrowLeftIcon className="size-3" />
|
||||
Back to Factions
|
||||
</Link>
|
||||
</div>
|
||||
{coverImageUrl ? (
|
||||
<img
|
||||
src={coverImageUrl}
|
||||
alt=""
|
||||
fetchPriority="high"
|
||||
className="absolute inset-0 size-full object-cover opacity-70"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-muted/40 to-background" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/40 to-background" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-black/30 via-transparent to-black/40" />
|
||||
|
||||
<div className="absolute top-4 right-4 z-20 flex gap-2">
|
||||
<span
|
||||
className={`rounded-md border border-white/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-wider backdrop-blur-sm ${typeColors[faction.type] ?? "bg-muted text-muted-foreground"}`}
|
||||
>
|
||||
{typeLabels[faction.type] ?? faction.type}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-md border border-white/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-wider backdrop-blur-sm ${reputationTone(reputation)}`}
|
||||
>
|
||||
{reputationLabel(reputation)} · {reputation >= 0 ? `+${reputation}` : reputation}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center p-6 md:p-10 lg:p-12 text-center">
|
||||
<h1
|
||||
id="faction-title"
|
||||
className="text-5xl font-bold tracking-tight text-white drop-shadow-2xl md:text-6xl"
|
||||
>
|
||||
{faction.name}
|
||||
</h1>
|
||||
<p className="mt-3 max-w-3xl text-lg leading-relaxed text-white/80">{heroSubtitle}</p>
|
||||
<div className="mt-6 flex items-center gap-2 text-sm text-white/70">
|
||||
<ShieldIcon className="size-4 text-white/60" />
|
||||
<span>{typeLabels[faction.type] ?? faction.type}</span>
|
||||
{faction.playableFaction && (
|
||||
<>
|
||||
<span className="text-white/30">·</span>
|
||||
<span>Playable</span>
|
||||
</>
|
||||
)}
|
||||
{faction.startingFaction && (
|
||||
<>
|
||||
<span className="text-white/30">·</span>
|
||||
<span>Starting</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-5 flex flex-col gap-6 max-w-7xl mx-auto w-full">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
{/* ── Main Content (2/3) ──────────────────────────── */}
|
||||
<div className="xl:col-span-2 flex flex-col gap-6">
|
||||
{faction.description && (
|
||||
<Item variant="outline">
|
||||
<ItemHeader>
|
||||
<ItemTitle>Description</ItemTitle>
|
||||
</ItemHeader>
|
||||
<ItemContent className="prose-invert prose-sm max-w-none [&_p]:mb-2 [&_ul]:list-disc [&_ul]:ml-4 [&_ol]:list-decimal [&_ol]:ml-4 [&_strong]:text-foreground [&_em]:italic">
|
||||
<RichText data={faction.description} />
|
||||
</ItemContent>
|
||||
</Item>
|
||||
)}
|
||||
|
||||
{relationships.length > 0 && (
|
||||
<Item variant="outline">
|
||||
<ItemHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<ScaleIcon className="size-4 text-muted-foreground" />
|
||||
<ItemTitle>Relationships</ItemTitle>
|
||||
</div>
|
||||
</ItemHeader>
|
||||
<ItemContent>
|
||||
<div className="flex flex-col gap-3">
|
||||
{relationships.map((r) => {
|
||||
const target = r.targetFaction as Faction;
|
||||
const value = r.value;
|
||||
const tone = relationshipTone(value);
|
||||
const barWidth = Math.min(Math.abs(value), 100);
|
||||
return (
|
||||
<Link
|
||||
key={r.id ?? target.id}
|
||||
href={`/intelligence/factions/${target.id}`}
|
||||
className="flex flex-col gap-1 rounded-md border border-border/50 p-3 transition-colors hover:border-primary/40 hover:bg-muted/30"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Link
|
||||
href={`/intelligence/factions/${target.id}`}
|
||||
className="flex items-center gap-2 text-sm font-medium hover:text-primary transition-colors"
|
||||
>
|
||||
<ShieldIcon className="size-4 text-muted-foreground" />
|
||||
<span>{target.name}</span>
|
||||
</Link>
|
||||
<span className={`text-xs font-mono ${tone.text}`}>
|
||||
{value >= 0 ? `+${value}` : value} · {tone.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className={`absolute top-0 h-full ${tone.bar}`}
|
||||
style={{
|
||||
left: value < 0 ? "50%" : `${50 - barWidth / 2}%`,
|
||||
width: `${barWidth / 2}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="absolute left-1/2 top-0 h-full w-px bg-border" />
|
||||
</div>
|
||||
{r.notes && (
|
||||
<p className="text-xs text-muted-foreground mt-1">{r.notes}</p>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
)}
|
||||
|
||||
{missions.length > 0 && (
|
||||
<section className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Missions
|
||||
</h2>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{missions.length} mission{missions.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{missions.map((mission) => (
|
||||
<Link key={mission.id} href={`/intelligence/missions/${mission.id}`}>
|
||||
<Item
|
||||
variant="outline"
|
||||
className="cursor-pointer group transition-all duration-200 hover:scale-[1.01]"
|
||||
>
|
||||
<ItemHeader>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
{mission.codeName}
|
||||
</span>
|
||||
<ItemTitle>{mission.name}</ItemTitle>
|
||||
</div>
|
||||
<span
|
||||
className={`text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded ${
|
||||
missionStatusColors[mission.ownershipAndStatus.status] ??
|
||||
"bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{mission.ownershipAndStatus.status}
|
||||
</span>
|
||||
</ItemHeader>
|
||||
<ItemContent>
|
||||
<p className="text-sm text-muted-foreground line-clamp-1">
|
||||
{mission.summary}
|
||||
</p>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Right Sidebar (1/3) ─────────────────────────── */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<Item variant="outline">
|
||||
<ItemHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<ScaleIcon className="size-4 text-muted-foreground" />
|
||||
<ItemTitle>Standing</ItemTitle>
|
||||
</div>
|
||||
</ItemHeader>
|
||||
<ItemContent>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Reputation</span>
|
||||
<span className={`font-mono ${reputationTone(reputation).split(" ")[1]}`}>
|
||||
{reputation >= 0 ? `+${reputation}` : reputation}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-2 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className={`absolute top-0 h-full ${reputationTone(reputation).split(" ")[0]}`}
|
||||
style={{
|
||||
left:
|
||||
reputation < 0
|
||||
? "50%"
|
||||
: `${50 - Math.min(Math.abs(reputation), 100) / 2}%`,
|
||||
width: `${Math.min(Math.abs(reputation), 100) / 2}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="absolute left-1/2 top-0 h-full w-px bg-border" />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pt-1">
|
||||
{reputationLabel(reputation)} standing in the world.
|
||||
</p>
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
|
||||
{bankAccount && (
|
||||
<Item variant="outline">
|
||||
<ItemHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<LandmarkIcon className="size-4 text-muted-foreground" />
|
||||
<ItemTitle>Bank Balance</ItemTitle>
|
||||
</div>
|
||||
<Link
|
||||
href={`/logistics/banking/${bankAccount.id}`}
|
||||
className="text-xs text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
</ItemHeader>
|
||||
<ItemContent>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-lg font-semibold font-mono">
|
||||
{formatAmount(bankAccount.balance ?? 0, bankCurrency)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{bankAccount.name}</span>
|
||||
{bankAccount.status !== "open" && (
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded w-fit bg-amber-500/20 text-amber-400">
|
||||
{bankAccount.status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
)}
|
||||
|
||||
{campaigns.length > 0 && (
|
||||
<Item variant="outline">
|
||||
<ItemHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<FlagIcon className="size-4 text-muted-foreground" />
|
||||
<ItemTitle>Associated Campaigns</ItemTitle>
|
||||
</div>
|
||||
</ItemHeader>
|
||||
<ItemContent>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{campaigns.map((c) => {
|
||||
const campaign = c as {
|
||||
id: number;
|
||||
name: string;
|
||||
status?: string;
|
||||
summary?: string;
|
||||
};
|
||||
return (
|
||||
<Link
|
||||
key={campaign.id}
|
||||
href={`/intelligence/campaigns/${campaign.id}`}
|
||||
className="flex items-center gap-3 p-1.5 rounded-md hover:bg-muted/50 transition-colors group"
|
||||
>
|
||||
<FlagIcon className="size-4 text-muted-foreground shrink-0" />
|
||||
<div className="flex flex-col gap-0.5 min-w-0 flex-1">
|
||||
<span className="text-sm font-medium truncate group-hover:text-primary transition-colors">
|
||||
{campaign.name}
|
||||
</span>
|
||||
{campaign.summary && (
|
||||
<span className="text-xs text-muted-foreground line-clamp-1">
|
||||
{campaign.summary}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{campaign.status && (
|
||||
<span
|
||||
className={`text-[10px] font-semibold uppercase tracking-wider px-1 py-0.5 rounded shrink-0 ${campaignStatusColors[campaign.status] ?? "bg-muted text-muted-foreground"}`}
|
||||
>
|
||||
{campaign.status === "inProgress"
|
||||
? "In Progress"
|
||||
: campaign.status.charAt(0).toUpperCase() +
|
||||
campaign.status.slice(1)}
|
||||
</span>
|
||||
)}
|
||||
<ChevronRightIcon className="size-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
|
|
@ -15,7 +16,7 @@ interface FactionCardProps {
|
|||
faction: {
|
||||
id: number;
|
||||
name: string;
|
||||
type: "majorFaction" | "minorFaction" | "corporation" | "special";
|
||||
type: "playerFaction" | "majorFaction" | "minorFaction" | "corporation" | "special";
|
||||
description?: {
|
||||
root: {
|
||||
type: string;
|
||||
|
|
@ -34,10 +35,12 @@ interface FactionCardProps {
|
|||
playableFaction?: boolean | null;
|
||||
startingFaction?: boolean | null;
|
||||
associatedCampaigns?: (number | { id: number; name: string })[] | null;
|
||||
reputation?: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
playerFaction: "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30",
|
||||
majorFaction: "bg-blue-500/20 text-blue-400",
|
||||
minorFaction: "bg-muted text-muted-foreground",
|
||||
corporation: "bg-amber-500/20 text-amber-400",
|
||||
|
|
@ -45,12 +48,29 @@ const typeColors: Record<string, string> = {
|
|||
};
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
playerFaction: "Player Faction",
|
||||
majorFaction: "Major Faction",
|
||||
minorFaction: "Minor Faction",
|
||||
corporation: "Corporation",
|
||||
special: "Special",
|
||||
};
|
||||
|
||||
function reputationTone(rep: number): string {
|
||||
if (rep >= 50) return "bg-emerald-500/20 text-emerald-400";
|
||||
if (rep >= 20) return "bg-green-500/20 text-green-400";
|
||||
if (rep > -20) return "bg-muted text-muted-foreground";
|
||||
if (rep > -50) return "bg-amber-500/20 text-amber-400";
|
||||
return "bg-red-500/20 text-red-400";
|
||||
}
|
||||
|
||||
function reputationLabel(rep: number): string {
|
||||
if (rep >= 50) return "Revered";
|
||||
if (rep >= 20) return "Respected";
|
||||
if (rep > -20) return "Neutral";
|
||||
if (rep > -50) return "Disliked";
|
||||
return "Reviled";
|
||||
}
|
||||
|
||||
export function FactionCard({ faction }: FactionCardProps) {
|
||||
const description = faction.description
|
||||
? convertLexicalToPlaintext({ data: faction.description })
|
||||
|
|
@ -61,9 +81,14 @@ export function FactionCard({ faction }: FactionCardProps) {
|
|||
: description;
|
||||
|
||||
const campaignCount = faction.associatedCampaigns?.length ?? 0;
|
||||
const reputation = faction.reputation ?? 0;
|
||||
|
||||
return (
|
||||
<Item variant="outline">
|
||||
<Link href={`/intelligence/factions/${faction.id}`} className="block">
|
||||
<Item
|
||||
variant="outline"
|
||||
className="cursor-pointer group transition-all duration-200 hover:scale-[1.01] hover:[box-shadow:inset_0_2px_8px_rgba(255,255,255,0.33)]"
|
||||
>
|
||||
<ItemHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<ItemMedia variant="icon">
|
||||
|
|
@ -80,7 +105,9 @@ export function FactionCard({ faction }: FactionCardProps) {
|
|||
<ItemContent>
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
{truncatedDescription && (
|
||||
<p className="text-sm text-muted-foreground">{truncatedDescription}</p>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{truncatedDescription}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{faction.playableFaction && (
|
||||
|
|
@ -93,6 +120,11 @@ export function FactionCard({ faction }: FactionCardProps) {
|
|||
Starting
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded ${reputationTone(reputation)}`}
|
||||
>
|
||||
{reputationLabel(reputation)} ({reputation >= 0 ? `+${reputation}` : reputation})
|
||||
</span>
|
||||
{campaignCount > 0 && (
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<UsersIcon className="size-3" />
|
||||
|
|
@ -106,5 +138,6 @@ export function FactionCard({ faction }: FactionCardProps) {
|
|||
{truncatedDescription ?? "No description provided."}
|
||||
</ItemDescription>
|
||||
</Item>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue