Compare commits
No commits in common. "09b074734f8827c23e082a6d2ff3091da6dd1610" and "42cbcbac3c3840cb58ec54212b8eae876bf0b360" have entirely different histories.
09b074734f
...
42cbcbac3c
4 changed files with 50 additions and 544 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "polaris-task-force",
|
"name": "polaris-task-force",
|
||||||
"version": "0.2.27",
|
"version": "0.2.26",
|
||||||
"description": "Web app for the Arma 3 unit Polaris Task Force. Uses Next.js for client- and server-side rendering, with a Postgres database and some other neat libraries.",
|
"description": "Web app for the Arma 3 unit Polaris Task Force. Uses Next.js for client- and server-side rendering, with a Postgres database and some other neat libraries.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ export default async function CampaignsPage() {
|
||||||
name: true,
|
name: true,
|
||||||
summary: true,
|
summary: true,
|
||||||
status: true,
|
status: true,
|
||||||
campaignMode: true,
|
|
||||||
startDate: true,
|
startDate: true,
|
||||||
endDate: true,
|
endDate: true,
|
||||||
owner: true,
|
owner: true,
|
||||||
|
|
@ -57,9 +56,6 @@ export default async function CampaignsPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const missionCount = missionsRes.docs.length;
|
const missionCount = missionsRes.docs.length;
|
||||||
const completedMissions = missionsRes.docs.filter(
|
|
||||||
(mission) => mission.ownershipAndStatus.status === "Completed",
|
|
||||||
).length;
|
|
||||||
const totalPlayers = missionsRes.docs.reduce(
|
const totalPlayers = missionsRes.docs.reduce(
|
||||||
(total, mission) => total + mission.missionRoles.maxPlayers,
|
(total, mission) => total + mission.missionRoles.maxPlayers,
|
||||||
0,
|
0,
|
||||||
|
|
@ -85,7 +81,6 @@ export default async function CampaignsPage() {
|
||||||
...campaign,
|
...campaign,
|
||||||
coverImageUrl,
|
coverImageUrl,
|
||||||
missionCount,
|
missionCount,
|
||||||
completedMissions,
|
|
||||||
avgPlayers: missionCount === 0 ? 0 : totalPlayers / missionCount,
|
avgPlayers: missionCount === 0 ? 0 : totalPlayers / missionCount,
|
||||||
avgDurationMinutes: missionCount === 0 ? 0 : totalDurationMinutes / missionCount,
|
avgDurationMinutes: missionCount === 0 ? 0 : totalDurationMinutes / missionCount,
|
||||||
ownerName,
|
ownerName,
|
||||||
|
|
@ -93,15 +88,8 @@ export default async function CampaignsPage() {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const officialCampaigns = campaignsWithStats.filter(
|
|
||||||
(campaign) => campaign.campaignMode === "official",
|
|
||||||
);
|
|
||||||
const communityCampaigns = campaignsWithStats.filter(
|
|
||||||
(campaign) => campaign.campaignMode !== "official",
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-5 flex flex-col gap-8">
|
<div className="p-5 flex flex-col gap-6">
|
||||||
<div className="flex flex-row justify-between gap-2">
|
<div className="flex flex-row justify-between gap-2">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -118,46 +106,12 @@ export default async function CampaignsPage() {
|
||||||
<CreateCampaignDialog />
|
<CreateCampaignDialog />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{campaignsWithStats.length > 0 ? (
|
{sorted.length > 0 ? (
|
||||||
<>
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
{officialCampaigns.length > 0 && (
|
{campaignsWithStats.map((campaign) => (
|
||||||
<section className="flex flex-col items-center gap-4">
|
<CampaignCard key={campaign.id} campaign={campaign} />
|
||||||
<div className="flex flex-col items-center gap-1 text-center">
|
))}
|
||||||
<h2 className="text-xl font-semibold uppercase tracking-wider text-white">
|
</div>
|
||||||
Official Campaigns
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Command-sanctioned operations at the heart of the unit's story.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 w-full max-w-6xl mx-auto">
|
|
||||||
{officialCampaigns.map((campaign) => (
|
|
||||||
<CampaignCard key={campaign.id} campaign={campaign} size="lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
{officialCampaigns.length > 0 && communityCampaigns.length > 0 && (
|
|
||||||
<hr className="w-48 mx-auto border-border/60" />
|
|
||||||
)}
|
|
||||||
{communityCampaigns.length > 0 && (
|
|
||||||
<section className="flex flex-col items-center gap-4">
|
|
||||||
<div className="flex flex-col items-center gap-1 text-center">
|
|
||||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
|
||||||
Community Campaigns
|
|
||||||
</h2>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Player-made side operations and stories, open to everyone.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 w-full max-w-6xl mx-auto">
|
|
||||||
{communityCampaigns.map((campaign) => (
|
|
||||||
<CampaignCard key={campaign.id} campaign={campaign} size="sm" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
<p className="text-sm">No campaigns found.</p>
|
<p className="text-sm">No campaigns found.</p>
|
||||||
|
|
|
||||||
|
|
@ -841,292 +841,3 @@
|
||||||
.wiki-markdown .hljs-strong {
|
.wiki-markdown .hljs-strong {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glitch hover effect for official campaign card titles: fires once (~1s) on hover, then reverts. */
|
|
||||||
.glitch-hover {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glitch-ghost {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
text-wrap: balance;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glitch-shard-a {
|
|
||||||
color: #22d3ee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glitch-shard-b {
|
|
||||||
color: #f43f5e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glitch-shard-c {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .glitch-hover {
|
|
||||||
animation: glitch-jitter 1s ease-out 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .glitch-shard-a {
|
|
||||||
animation: glitch-shard-a 1s ease-out 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .glitch-shard-b {
|
|
||||||
animation: glitch-shard-b 1s ease-out 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .glitch-shard-c {
|
|
||||||
animation: glitch-shard-c 1s ease-out 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch-jitter {
|
|
||||||
0% {
|
|
||||||
transform: translate(3px, -2px) skewX(2deg);
|
|
||||||
text-shadow:
|
|
||||||
-4px 0 rgba(34, 211, 238, 0.7),
|
|
||||||
4px 0 rgba(244, 63, 94, 0.7);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
5% {
|
|
||||||
transform: translate(-3px, 2px) skewX(-2deg);
|
|
||||||
text-shadow:
|
|
||||||
3px 0 rgba(34, 211, 238, 0.7),
|
|
||||||
-3px 0 rgba(244, 63, 94, 0.7);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
10% {
|
|
||||||
transform: translate(2px, -1px) skewX(1deg);
|
|
||||||
text-shadow:
|
|
||||||
-2px 0 rgba(34, 211, 238, 0.6),
|
|
||||||
2px 0 rgba(244, 63, 94, 0.6);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
15% {
|
|
||||||
transform: translate(-1px, 1px);
|
|
||||||
text-shadow:
|
|
||||||
1px 0 rgba(34, 211, 238, 0.5),
|
|
||||||
-1px 0 rgba(244, 63, 94, 0.5);
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translate(0, 0) skewX(0deg);
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0, 0) skewX(0deg);
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch-shard-a {
|
|
||||||
0% {
|
|
||||||
opacity: 0.9;
|
|
||||||
clip-path: polygon(0 0, 62% 0, 54% 34%, 0 34%);
|
|
||||||
transform: translate(-8px, -4px);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
8% {
|
|
||||||
clip-path: polygon(6% 4%, 68% 0, 58% 38%, 0 30%);
|
|
||||||
transform: translate(8px, 4px);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
16% {
|
|
||||||
opacity: 0.35;
|
|
||||||
clip-path: polygon(0 0, 50% 6%, 44% 30%, 0 38%);
|
|
||||||
transform: translate(-3px, -2px);
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
35% {
|
|
||||||
opacity: 0;
|
|
||||||
clip-path: polygon(0 0, 62% 0, 54% 34%, 0 34%);
|
|
||||||
transform: translate(-5px, -3px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
clip-path: polygon(0 0, 62% 0, 54% 34%, 0 34%);
|
|
||||||
transform: translate(-5px, -3px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch-shard-b {
|
|
||||||
0% {
|
|
||||||
opacity: 0.9;
|
|
||||||
clip-path: polygon(38% 66%, 100% 58%, 100% 100%, 30% 100%);
|
|
||||||
transform: translate(8px, 4px);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
8% {
|
|
||||||
clip-path: polygon(32% 62%, 100% 66%, 100% 100%, 24% 96%);
|
|
||||||
transform: translate(-8px, -4px);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
16% {
|
|
||||||
opacity: 0.35;
|
|
||||||
clip-path: polygon(50% 70%, 100% 62%, 100% 100%, 44% 100%);
|
|
||||||
transform: translate(3px, 2px);
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
35% {
|
|
||||||
opacity: 0;
|
|
||||||
clip-path: polygon(38% 66%, 100% 58%, 100% 100%, 30% 100%);
|
|
||||||
transform: translate(5px, 3px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
clip-path: polygon(38% 66%, 100% 58%, 100% 100%, 30% 100%);
|
|
||||||
transform: translate(5px, 3px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch-shard-c {
|
|
||||||
0% {
|
|
||||||
opacity: 0.95;
|
|
||||||
clip-path: polygon(8% 42%, 96% 38%, 88% 62%, 0 66%);
|
|
||||||
transform: translate(6px, -4px);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
6% {
|
|
||||||
transform: translate(-6px, 4px);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
12% {
|
|
||||||
clip-path: polygon(0 46%, 88% 40%, 96% 58%, 4% 62%);
|
|
||||||
transform: translate(4px, 2px);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
transform: translate(-2px, -1px);
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
35% {
|
|
||||||
opacity: 0;
|
|
||||||
clip-path: polygon(8% 42%, 96% 38%, 88% 62%, 0 66%);
|
|
||||||
transform: translate(1px, 2px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
clip-path: polygon(8% 42%, 96% 38%, 88% 62%, 0 66%);
|
|
||||||
transform: translate(1px, 2px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .glitch-soft {
|
|
||||||
animation: glitch-soft 1s ease-out 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .glitch-soft-late {
|
|
||||||
animation-delay: 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch-soft {
|
|
||||||
0% {
|
|
||||||
transform: translate(1px, -1px);
|
|
||||||
text-shadow:
|
|
||||||
1px 0 rgba(34, 211, 238, 0.45),
|
|
||||||
-1px 0 rgba(244, 63, 94, 0.45);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
8% {
|
|
||||||
transform: translate(-1px, 1px);
|
|
||||||
text-shadow:
|
|
||||||
-1px 0 rgba(34, 211, 238, 0.45),
|
|
||||||
1px 0 rgba(244, 63, 94, 0.45);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
16% {
|
|
||||||
transform: translate(1px, 1px);
|
|
||||||
animation-timing-function: ease-out;
|
|
||||||
}
|
|
||||||
30% {
|
|
||||||
transform: translate(0, 0);
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0, 0);
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.glitch-ghost-img {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .glitch-ghost-img-a {
|
|
||||||
animation: glitch-img-band-a 2s ease-out 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .glitch-ghost-img-b {
|
|
||||||
animation: glitch-img-band-b 2s ease-out 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch-img-band-a {
|
|
||||||
0% {
|
|
||||||
opacity: 0.45;
|
|
||||||
clip-path: inset(0 0 55% 0);
|
|
||||||
transform: translate(-4px, 0);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
6% {
|
|
||||||
clip-path: inset(5% 0 60% 0);
|
|
||||||
transform: translate(4px, 2px);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
12% {
|
|
||||||
opacity: 0.35;
|
|
||||||
clip-path: inset(10% 0 62% 0);
|
|
||||||
transform: translate(-2px, 0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
clip-path: inset(0 0 55% 0);
|
|
||||||
transform: translate(0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glitch-img-band-b {
|
|
||||||
0% {
|
|
||||||
opacity: 0.45;
|
|
||||||
clip-path: inset(45% 0 0 0);
|
|
||||||
transform: translate(4px, 0);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
6% {
|
|
||||||
clip-path: inset(40% 0 5% 0);
|
|
||||||
transform: translate(-4px, -2px);
|
|
||||||
animation-timing-function: steps(1, end);
|
|
||||||
}
|
|
||||||
12% {
|
|
||||||
opacity: 0.35;
|
|
||||||
clip-path: inset(38% 0 10% 0);
|
|
||||||
transform: translate(2px, 0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
clip-path: inset(45% 0 0 0);
|
|
||||||
transform: translate(0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.group:hover .glitch-hover {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .glitch-ghost {
|
|
||||||
animation: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .glitch-soft {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .glitch-ghost-img {
|
|
||||||
animation: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ interface CampaignCardCampaign extends Pick<
|
||||||
> {
|
> {
|
||||||
readonly coverImageUrl: string | null;
|
readonly coverImageUrl: string | null;
|
||||||
readonly missionCount: number;
|
readonly missionCount: number;
|
||||||
readonly completedMissions: number;
|
|
||||||
readonly avgPlayers: number;
|
readonly avgPlayers: number;
|
||||||
readonly avgDurationMinutes: number;
|
readonly avgDurationMinutes: number;
|
||||||
}
|
}
|
||||||
|
|
@ -20,7 +19,6 @@ type CampaignCardInput = CampaignCardCampaign | Campaign;
|
||||||
|
|
||||||
interface CampaignCardProps {
|
interface CampaignCardProps {
|
||||||
readonly campaign: CampaignCardInput;
|
readonly campaign: CampaignCardInput;
|
||||||
readonly size?: "lg" | "sm";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColors: Record<Campaign["status"], string> = {
|
const statusColors: Record<Campaign["status"], string> = {
|
||||||
|
|
@ -35,20 +33,7 @@ const statusLabels: Record<Campaign["status"], string> = {
|
||||||
completed: "Completed",
|
completed: "Completed",
|
||||||
};
|
};
|
||||||
|
|
||||||
const officialStatusLabels: Record<Campaign["status"], string> = {
|
export function CampaignCard({ campaign }: CampaignCardProps) {
|
||||||
concept: "Planning",
|
|
||||||
inProgress: "Active Deployment",
|
|
||||||
completed: "Concluded",
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusTextColors: Record<Campaign["status"], string> = {
|
|
||||||
concept: "text-muted-foreground",
|
|
||||||
inProgress: "text-blue-400",
|
|
||||||
completed: "text-emerald-400",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CampaignCard({ campaign, size = "lg" }: CampaignCardProps) {
|
|
||||||
const isSmall = size === "sm";
|
|
||||||
const coverImageUrl =
|
const coverImageUrl =
|
||||||
"coverImageUrl" in campaign
|
"coverImageUrl" in campaign
|
||||||
? campaign.coverImageUrl
|
? campaign.coverImageUrl
|
||||||
|
|
@ -56,8 +41,6 @@ export function CampaignCard({ campaign, size = "lg" }: CampaignCardProps) {
|
||||||
? (campaign.coverImage.url ?? null)
|
? (campaign.coverImage.url ?? null)
|
||||||
: null;
|
: null;
|
||||||
const missionCount = "missionCount" in campaign ? campaign.missionCount : 0;
|
const missionCount = "missionCount" in campaign ? campaign.missionCount : 0;
|
||||||
const completedMissions =
|
|
||||||
"completedMissions" in campaign ? campaign.completedMissions : 0;
|
|
||||||
const avgPlayers = "avgPlayers" in campaign ? campaign.avgPlayers : 0;
|
const avgPlayers = "avgPlayers" in campaign ? campaign.avgPlayers : 0;
|
||||||
const avgDurationMinutes = "avgDurationMinutes" in campaign ? campaign.avgDurationMinutes : 0;
|
const avgDurationMinutes = "avgDurationMinutes" in campaign ? campaign.avgDurationMinutes : 0;
|
||||||
|
|
||||||
|
|
@ -74,56 +57,21 @@ export function CampaignCard({ campaign, size = "lg" }: CampaignCardProps) {
|
||||||
? owner.displayName
|
? owner.displayName
|
||||||
: "Unknown";
|
: "Unknown";
|
||||||
|
|
||||||
const refCode = String(campaign.id).padStart(3, "0");
|
|
||||||
const statusLabel = isSmall ? statusLabels[campaign.status] : officialStatusLabels[campaign.status];
|
|
||||||
const readoutCells = [
|
|
||||||
{ label: "Msns", value: missionCount.toString(), mono: true },
|
|
||||||
{ label: "Done", value: completedMissions.toString(), mono: true },
|
|
||||||
{ label: "Avg Squad", value: playersLabel, mono: true },
|
|
||||||
{ label: "Duration", value: durationLabel, mono: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/intelligence/campaigns/${campaign.id}`}
|
href={`/intelligence/campaigns/${campaign.id}`}
|
||||||
aria-label={`Open campaign ${campaign.name}`}
|
aria-label={`Open campaign ${campaign.name}`}
|
||||||
className="block rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
className="block rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
||||||
>
|
>
|
||||||
<article
|
<article className="group relative aspect-[4/3] overflow-hidden rounded-lg border border-border/70 bg-card transition-colors duration-300 group-hover:border-foreground/30">
|
||||||
className={cn(
|
|
||||||
"group relative overflow-hidden border border-border/70 bg-card transition-colors duration-300 group-hover:border-foreground/30",
|
|
||||||
isSmall ? "aspect-[16/9] rounded-lg" : "aspect-[4/3] rounded-none",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 scale-100 transition-transform duration-500 ease-out group-hover:scale-110 motion-reduce:transition-none motion-reduce:group-hover:scale-100">
|
<div className="absolute inset-0 scale-100 transition-transform duration-500 ease-out group-hover:scale-110 motion-reduce:transition-none motion-reduce:group-hover:scale-100">
|
||||||
{coverImageUrl !== null ? (
|
{coverImageUrl !== null ? (
|
||||||
<>
|
<img
|
||||||
<img
|
src={coverImageUrl}
|
||||||
src={coverImageUrl}
|
alt=""
|
||||||
alt=""
|
aria-hidden="true"
|
||||||
aria-hidden="true"
|
className="absolute inset-0 size-full object-cover opacity-30 transition-opacity duration-500 ease-out group-hover:opacity-50 motion-reduce:transition-none"
|
||||||
className={cn(
|
/>
|
||||||
"absolute inset-0 size-full object-cover transition-opacity duration-500 ease-out motion-reduce:transition-none",
|
|
||||||
isSmall ? "opacity-30 group-hover:opacity-50" : "opacity-15 group-hover:opacity-25",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{!isSmall && (
|
|
||||||
<>
|
|
||||||
<img
|
|
||||||
src={coverImageUrl}
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
className="glitch-ghost-img glitch-ghost-img-a absolute inset-0 size-full object-cover"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src={coverImageUrl}
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
className="glitch-ghost-img glitch-ghost-img-b absolute inset-0 size-full object-cover"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|
@ -134,159 +82,52 @@ export function CampaignCard({ campaign, size = "lg" }: CampaignCardProps) {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn(
|
className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent"
|
||||||
"absolute inset-0 bg-gradient-to-t",
|
|
||||||
isSmall ? "from-black/80 via-black/40 to-transparent" : "from-black/85 via-black/60 to-black/25",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isSmall && (
|
<span
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="pointer-events-none absolute inset-0 z-[5] opacity-[0.04] transition-opacity duration-500 group-hover:opacity-[0.07] motion-reduce:transition-none"
|
|
||||||
style={{
|
|
||||||
backgroundImage:
|
|
||||||
"linear-gradient(to right, white 1px, transparent 1px), linear-gradient(to bottom, white 1px, transparent 1px)",
|
|
||||||
backgroundSize: "28px 28px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSmall && (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"absolute right-3 top-3 z-10 rounded-full px-2 py-1 text-[10px] font-semibold uppercase tracking-wider",
|
|
||||||
statusColors[campaign.status],
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{statusLabel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isSmall && (
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute inset-x-0 top-0 z-20 flex items-center justify-between border-b border-white/15 bg-black/50 px-3 py-1.5 backdrop-blur-sm"
|
|
||||||
>
|
|
||||||
<span className="font-mono text-[9px] uppercase tracking-widest text-white/60">
|
|
||||||
Official // Unit Command
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-[9px] uppercase tracking-widest text-white/60">
|
|
||||||
REF {refCode}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isSmall && (
|
|
||||||
<div aria-hidden="true" className="pointer-events-none absolute inset-2 z-30">
|
|
||||||
<span className="absolute left-0 top-0 size-3 border-l border-t border-white/25 transition-colors duration-300 group-hover:border-white/60" />
|
|
||||||
<span className="absolute right-0 top-0 size-3 border-r border-t border-white/25 transition-colors duration-300 group-hover:border-white/60" />
|
|
||||||
<span className="absolute bottom-0 left-0 size-3 border-b border-l border-white/25 transition-colors duration-300 group-hover:border-white/60" />
|
|
||||||
<span className="absolute bottom-0 right-0 size-3 border-b border-r border-white/25 transition-colors duration-300 group-hover:border-white/60" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-0 z-10 flex items-center justify-center text-center",
|
"absolute right-3 top-3 z-10 rounded-full px-2 py-1 text-[10px] font-semibold uppercase tracking-wider",
|
||||||
isSmall ? "px-12 pb-6 pt-8" : "px-10",
|
statusColors[campaign.status],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{statusLabels[campaign.status]}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 z-10 flex items-center justify-center px-6 pb-6 text-center">
|
||||||
<div className="flex max-w-full flex-col items-center gap-2">
|
<div className="flex max-w-full flex-col items-center gap-2">
|
||||||
<h2
|
<h2 className="text-balance text-center text-xl font-bold text-white drop-shadow-lg">
|
||||||
data-text={campaign.name}
|
|
||||||
className={cn(
|
|
||||||
"text-balance text-center font-bold text-white drop-shadow-lg",
|
|
||||||
isSmall ? "text-lg" : "glitch-hover text-xl",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{campaign.name}
|
{campaign.name}
|
||||||
{!isSmall && (
|
|
||||||
<>
|
|
||||||
<span aria-hidden="true" className="glitch-ghost glitch-shard-a">
|
|
||||||
{campaign.name}
|
|
||||||
</span>
|
|
||||||
<span aria-hidden="true" className="glitch-ghost glitch-shard-b">
|
|
||||||
{campaign.name}
|
|
||||||
</span>
|
|
||||||
<span aria-hidden="true" className="glitch-ghost glitch-shard-c">
|
|
||||||
{campaign.name}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</h2>
|
</h2>
|
||||||
{campaign.summary && (
|
<p className="text-sm uppercase tracking-wider text-white/70 drop-shadow">
|
||||||
<p
|
{statusLabels[campaign.status]}
|
||||||
className={cn(
|
</p>
|
||||||
"line-clamp-2 max-w-prose text-pretty text-white/70 drop-shadow",
|
|
||||||
isSmall ? "text-xs" : "glitch-soft text-sm",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{campaign.summary}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{!isSmall && (
|
|
||||||
<p
|
|
||||||
className={cn(
|
|
||||||
"glitch-soft glitch-soft-late uppercase tracking-wider drop-shadow",
|
|
||||||
statusTextColors[campaign.status],
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{officialStatusLabels[campaign.status]}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSmall ? (
|
<footer className="absolute inset-x-0 bottom-0 z-20 flex translate-y-4 items-center bg-black/60 px-2 py-2.5 opacity-0 backdrop-blur-sm transition-all duration-300 ease-out group-hover:translate-y-0 group-hover:opacity-100 motion-reduce:transition-none">
|
||||||
<footer className="absolute inset-x-0 bottom-0 z-20 flex translate-y-4 items-center bg-black/60 px-2 py-2.5 opacity-0 backdrop-blur-sm transition-all duration-300 ease-out group-hover:translate-y-0 group-hover:opacity-100 motion-reduce:transition-none">
|
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 border-r border-white/15 text-xs text-white/80">
|
||||||
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 border-r border-white/15 text-xs text-white/80">
|
<CrosshairIcon aria-hidden="true" className="size-3.5 shrink-0 text-white/60" />
|
||||||
<CrosshairIcon aria-hidden="true" className="size-3.5 shrink-0 text-white/60" />
|
<span className="sr-only">Missions</span>
|
||||||
<span className="sr-only">Missions</span>
|
<span className="truncate">{missionCount}</span>
|
||||||
<span className="truncate">{missionCount}</span>
|
</div>
|
||||||
|
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 border-r border-white/15 text-xs text-white/80">
|
||||||
|
<UsersIcon aria-hidden="true" className="size-3.5 shrink-0 text-white/60" />
|
||||||
|
<span className="sr-only">Avg players</span>
|
||||||
|
<span className="truncate">{playersLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 text-xs text-white/80">
|
||||||
|
<ClockIcon aria-hidden="true" className="size-3.5 shrink-0 text-white/60" />
|
||||||
|
<span className="sr-only">Avg duration</span>
|
||||||
|
<span className="truncate">{durationLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 text-xs text-white/80">
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="truncate">{ownerName}</div>
|
||||||
|
<span className="text-[10px] uppercase tracking-wider text-white/60">Owner</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 border-r border-white/15 text-xs text-white/80">
|
</div>
|
||||||
<UsersIcon aria-hidden="true" className="size-3.5 shrink-0 text-white/60" />
|
</footer>
|
||||||
<span className="sr-only">Avg players</span>
|
|
||||||
<span className="truncate">{playersLabel}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 text-xs text-white/80">
|
|
||||||
<ClockIcon aria-hidden="true" className="size-3.5 shrink-0 text-white/60" />
|
|
||||||
<span className="sr-only">Avg duration</span>
|
|
||||||
<span className="truncate">{durationLabel}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 text-xs text-white/80">
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="truncate">{ownerName}</div>
|
|
||||||
<span className="text-[10px] uppercase tracking-wider text-white/60">Owner</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
) : (
|
|
||||||
<footer className="absolute inset-x-0 bottom-0 z-20 flex items-stretch border-t border-white/15 bg-black/60 px-2 py-1.5 backdrop-blur-sm">
|
|
||||||
{readoutCells.map((cell, index) => (
|
|
||||||
<div
|
|
||||||
key={cell.label}
|
|
||||||
className={cn(
|
|
||||||
"flex min-w-0 flex-1 items-center justify-center gap-1.5 px-1",
|
|
||||||
index < readoutCells.length - 1 && "border-r border-white/15",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"truncate text-xs text-white/80",
|
|
||||||
cell.mono && "font-mono tabular-nums",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{cell.value}
|
|
||||||
</span>
|
|
||||||
<span className="shrink-0 text-[9px] uppercase tracking-wider text-white/50">
|
|
||||||
{cell.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</footer>
|
|
||||||
)}
|
|
||||||
</article>
|
</article>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue