Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/deploy-v3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ jobs:
deploy-v3:
runs-on: ubuntu-latest
environment: v3-deployment
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "20"
- name: Install yarn
run: npm install -g yarn
- name: Restore cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
.next/cache
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ jobs:
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
environment: preview-deployment
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
Comment on lines +18 to 20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Detect risky combination: pull_request_target + checkout of PR head + secrets usage
rg -n -C2 'pull_request_target|ref:\s*\$\{\{\s*github\.event\.pull_request\.head\.sha\s*\}\}|apiToken:\s*\$\{\{\s*secrets\.' .github/workflows

Repository: codse/animata

Length of output: 1471


🏁 Script executed:

cat -n .github/workflows/deploy.yml

Repository: codse/animata

Length of output: 2983


Disable fork execution in this privileged workflow.

Line 11 explicitly runs this job only for forks (!= github.repository), while checking out untrusted fork code at line 20 with access to deployment secrets (CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, GITHUB_TOKEN). This is a critical secret-exfiltration vector.

Change line 11 from:

    if: github.event.pull_request.head.repo.full_name != github.repository

to:

    if: github.event.pull_request.head.repo.full_name == github.repository

This restricts preview deployment to internal PRs only. If fork previews are required, split into an unprivileged pull_request build and a maintainer-approved privileged deploy path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/deploy.yml around lines 18 - 20, The workflow currently
checks out pull request head from forks with privileged secrets available;
update the job condition that uses github.event.pull_request.head.repo.full_name
so it only runs for the same-repository PRs (change the negated check to an
equality) and stop checking out untrusted fork code in the privileged job (the
checkout step referencing actions/checkout@v6 and ref: ${{
github.event.pull_request.head.sha }}). If fork previews are needed, split into
an unprivileged pull_request job (no deployment secrets) and a separate
maintainer-approved privileged deploy job that uses CLOUDFLARE_API_TOKEN /
CLOUDFLARE_ACCOUNT_ID / GITHUB_TOKEN only after manual approval.

- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "22"
- name: Install yarn
run: npm install -g yarn
- name: Restore cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
.next/cache
Expand Down
33 changes: 33 additions & 0 deletions animata/card/state-action-card.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from "@storybook/react";

import StateActionCard from "@/animata/card/state-action-card";

const meta = {
title: "Card/State Action Card",
component: StateActionCard,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
} satisfies Meta<typeof StateActionCard>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Taskmanager: Story = {
args: {
useCase: "task",
},
};

export const Socialcard: Story = {
args: {
useCase: "social",
},
};

export const Ordercard: Story = {
args: {
useCase: "order",
},
};
271 changes: 271 additions & 0 deletions animata/card/state-action-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
"use client";

import {
Check,
CheckCircle2,
ClipboardList,
Heart,
Package,
Share2,
Sparkles,
Users,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useMemo, useState } from "react";

import { cn } from "@/lib/utils";

type CardUseCase = "task" | "social" | "order";

type ActionType = "favorite" | "complete" | "share";

interface CardPreset {
title: string;
description: string;
meta: string;
badge: string;
icon: typeof ClipboardList;
}

interface StateActionCardProps {
readonly useCase?: CardUseCase;
readonly className?: string;
}

const cardPresets: Record<CardUseCase, CardPreset> = {
task: {
title: "Finalize Sprint Notes",
description: "Wrap up pending checklist items and post a summary for the team standup.",
meta: "Due in 3 hours",
badge: "Task Manager",
icon: ClipboardList,
},
social: {
title: "Design Community Spotlight",
description: "A new behind-the-scenes post is trending. Save it or share it with your team.",
meta: "2.4k interactions",
badge: "Social Card",
icon: Users,
},
order: {
title: "Order #48291",
description: "Wireless Keyboard and Mouse bundle is packed and ready for final dispatch.",
meta: "Ships today",
badge: "Dashboard Order",
icon: Package,
},
};

const confettiPieces = [
{ id: "c1", x: -48, y: -34, rotate: -35, color: "bg-emerald-400" },
{ id: "c2", x: -26, y: -50, rotate: -10, color: "bg-cyan-400" },
{ id: "c3", x: -6, y: -56, rotate: 6, color: "bg-yellow-400" },
{ id: "c4", x: 18, y: -50, rotate: 22, color: "bg-fuchsia-400" },
{ id: "c5", x: 42, y: -34, rotate: 38, color: "bg-orange-400" },
{ id: "c6", x: -36, y: -18, rotate: -24, color: "bg-lime-400" },
{ id: "c7", x: 32, y: -16, rotate: 30, color: "bg-sky-400" },
{ id: "c8", x: 0, y: -30, rotate: 0, color: "bg-violet-400" },
];

export default function StateActionCard({
useCase = "task",
className,
}: Readonly<StateActionCardProps>) {
const preset = cardPresets[useCase];
const CardIcon = preset.icon;

const [isFavorite, setIsFavorite] = useState(false);
const [isCompleted, setIsCompleted] = useState(false);
const [isShared, setIsShared] = useState(false);
const [lastAction, setLastAction] = useState<ActionType | null>(null);
const [showConfetti, setShowConfetti] = useState(false);

const statuses = useMemo(() => {
return [
{ label: preset.badge, className: "bg-zinc-900 text-white" },
isCompleted
? { label: "Completed", className: "bg-emerald-100 text-emerald-700" }
: { label: "In Progress", className: "bg-amber-100 text-amber-700" },
isFavorite
? { label: "Favorited", className: "bg-rose-100 text-rose-700" }
: { label: "Not Favorite", className: "bg-zinc-100 text-zinc-600" },
isShared
? { label: "Shared", className: "bg-sky-100 text-sky-700" }
: { label: "Private", className: "bg-zinc-100 text-zinc-600" },
];
}, [isCompleted, isFavorite, isShared, preset.badge]);

const triggerActionFeedback = (action: ActionType) => {
setLastAction(action);
window.setTimeout(() => {
setLastAction((previous) => (previous === action ? null : previous));
}, 800);
};

const onFavorite = () => {
setIsFavorite((previous) => !previous);
triggerActionFeedback("favorite");
};

const onComplete = () => {
const nextValue = !isCompleted;
setIsCompleted(nextValue);
triggerActionFeedback("complete");

if (nextValue) {
setShowConfetti(true);
window.setTimeout(() => {
setShowConfetti(false);
}, 1000);
}
};

const onShare = () => {
setIsShared((previous) => !previous);
triggerActionFeedback("share");
};

return (
<motion.article
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35 }}
className={cn(
"group relative w-full max-w-sm overflow-hidden rounded-2xl border border-zinc-200 bg-white p-5 shadow-[0_16px_45px_-24px_rgba(0,0,0,0.45)]",
className,
)}
>
<div className="absolute inset-x-0 top-0 h-1 bg-linear-to-r from-cyan-500 via-emerald-500 to-fuchsia-500" />

<div className="mb-4 flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<span className="rounded-xl bg-zinc-100 p-2 text-zinc-700">
<CardIcon className="size-4" />
</span>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-zinc-500">
Interactive Card
</p>
</div>
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-medium text-emerald-700">
<Sparkles className="size-3" />
Live State
</span>
</div>

<h3 className="text-xl font-semibold text-zinc-900">{preset.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-zinc-600">{preset.description}</p>

<div className="mt-4 flex flex-wrap gap-2">
{statuses.map((status) => (
<span
key={status.label}
className={cn("rounded-full px-2.5 py-1 text-xs font-medium", status.className)}
>
{status.label}
</span>
))}
</div>

<div className="mt-5 flex items-center justify-between">
<p className="text-sm font-medium text-zinc-500">{preset.meta}</p>

<AnimatePresence mode="wait" initial={false}>
{lastAction && (
<motion.div
key={lastAction}
initial={{ opacity: 0, y: 8, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -8, scale: 0.95 }}
transition={{ duration: 0.18 }}
className="inline-flex items-center gap-1 rounded-full bg-emerald-600 px-2.5 py-1 text-xs font-semibold text-white"
>
<Check className="size-3.5" />
Action saved
</motion.div>
)}
</AnimatePresence>
</div>

<div className="pointer-events-none mt-4 h-0.5 bg-linear-to-r from-transparent via-zinc-200 to-transparent" />

<div
className={cn(
"mt-4 flex items-center gap-2 transition-all duration-300",
"opacity-100 translate-y-0 sm:translate-y-3 sm:opacity-0 sm:group-hover:translate-y-0 sm:group-hover:opacity-100 sm:group-focus-within:translate-y-0 sm:group-focus-within:opacity-100",
)}
>
<ActionButton
icon={Heart}
onClick={onFavorite}
label={isFavorite ? "Favorited" : "Add to favorites"}
active={isFavorite}
/>

<ActionButton
icon={CheckCircle2}
onClick={onComplete}
label={isCompleted ? "Completed" : "Mark complete"}
active={isCompleted}
/>

<ActionButton
icon={Share2}
onClick={onShare}
label={isShared ? "Shared" : "Share"}
active={isShared}
/>
</div>

<AnimatePresence>
{showConfetti && (
<motion.div
className="pointer-events-none absolute left-1/2 top-[52%]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{confettiPieces.map((piece) => (
<motion.span
key={piece.id}
className={cn("absolute h-2 w-1.5 rounded-sm", piece.color)}
initial={{ x: 0, y: 0, rotate: 0, opacity: 1 }}
animate={{
x: piece.x,
y: piece.y,
rotate: piece.rotate,
opacity: 0,
}}
transition={{ duration: 0.75, ease: "easeOut" }}
/>
))}
</motion.div>
)}
</AnimatePresence>
</motion.article>
Comment on lines +128 to +244
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing dark mode support violates theme-responsive requirement.

The component uses only light-mode styles (bg-white, text-zinc-900, bg-zinc-100, etc.) without any dark: variants. All new components must be theme-responsive.

Key areas needing dark mode variants:

  • Line 134: bg-white → add dark:bg-zinc-900
  • Line 142: bg-zinc-100 → add dark:bg-zinc-800
  • Line 155: text-zinc-900 → add dark:text-zinc-100
  • Status badges (lines 85-94): add dark variants for each state
  • Action buttons (lines 260-265): add dark variants

As per coding guidelines: "All new components must be theme-responsive (light + dark mode support)"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@animata/card/state-action-card.tsx` around lines 128 - 244, The component
lacks dark-mode class variants; update the style classNames used in
motion.article, the CardIcon wrapper span, the title/description text, the
status badges rendered from statuses, and the ActionButton usages to include
appropriate dark: variants (e.g., add dark:bg-zinc-900 alongside bg-white on
motion.article; add dark:bg-zinc-800 to the CardIcon container; add
dark:text-zinc-100 for preset.title/preset.description; ensure each
status.className from statuses includes matching dark classes or map a darkClass
for each status; and pass dark-aware className props to ActionButton instances
used for Heart, CheckCircle2, Share2) so the card renders correctly in both
light and dark themes.

);
}

interface ActionButtonProps {
readonly icon: typeof Heart;
readonly label: string;
readonly active: boolean;
readonly onClick: () => void;
}

function ActionButton({ icon: Icon, label, active, onClick }: Readonly<ActionButtonProps>) {
return (
<button
type="button"
onClick={onClick}
className={cn(
"inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition",
active
? "border-emerald-200 bg-emerald-50 text-emerald-700"
: "border-zinc-200 bg-white text-zinc-600 hover:border-zinc-300 hover:bg-zinc-50",
)}
>
<Icon className="size-3.5" />
{label}
</button>
);
}
Loading
Loading