Skip to content
Merged
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
37 changes: 13 additions & 24 deletions .github/workflows/react-doctor.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,27 @@
name: React Doctor

on:
# Scans the PR's changed files and posts a sticky summary comment listing only the new issues introduced relative to the merge base of the target branch.
pull_request:
branches:
- main
push:
branches:
- main
branches: [main]
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read
pull-requests: write
issues: write
statuses: write

# Cancels any in-flight scan for the same PR the moment a new commit arrives, so reviewers only ever see the latest run.
concurrency:
group: react-doctor-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
react-doctor:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 11.5.0

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run React Doctor
run: pnpm dlx react-doctor@latest . -y --diff main --blocking error
- uses: millionco/react-doctor@v2
16 changes: 1 addition & 15 deletions animata/card/card-stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
useMemo,
useRef,
useState,
useSyncExternalStore,
} from "react";
import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion";
import { cn } from "@/lib/utils";

export interface CardStackItem {
Expand Down Expand Up @@ -87,20 +87,6 @@ export function createCardStackThrowImpulse(): CardStackThrowImpulse {
const CARD_STACK_STACK_ORIGIN = "50% 0%";
const CARD_STACK_EXIT_Y = "200%";

function subscribeReducedMotion(callback: () => void) {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
mq.addEventListener("change", callback);
return () => mq.removeEventListener("change", callback);
}

function getReducedMotionSnapshot() {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

function usePrefersReducedMotion() {
return useSyncExternalStore(subscribeReducedMotion, getReducedMotionSnapshot, () => false);
}

export function getCardStackLayers(
reducedMotion: boolean,
depth = DEFAULT_STACK_DEPTH,
Expand Down
54 changes: 42 additions & 12 deletions animata/card/flip-card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,54 @@ type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {
image:
" https://images.unsplash.com/photo-1525373698358-041e3a460346?w=600&auto=format&fit=crop&q=60&ixlib=rb-4.0.3",
title: "Programming",
subtitle: "What is programming?",
description:
"Computer programming or coding is the composition of sequences of instructions, called programs, that computers can follow to perform tasks.",
rotate: "y",
},
render: ({ rotate }) => (
<FlipCard rotate={rotate}>
<FlipCard.Front>
<img
src="https://images.unsplash.com/photo-1525373698358-041e3a460346?w=600&auto=format&fit=crop&q=60&ixlib=rb-4.0.3"
alt="Programming"
className="h-full w-full rounded-2xl object-cover shadow-2xl shadow-black/40"
/>
<div className="absolute bottom-4 left-4 text-xl font-bold text-white">Programming</div>
</FlipCard.Front>
<FlipCard.Back className="rounded-2xl bg-black/80 p-4 text-slate-200">
<div className="flex min-h-full flex-col gap-2">
<h1 className="text-base font-bold text-white">What is programming?</h1>
<p className="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100">
Computer programming or coding is the composition of sequences of instructions, called
programs, that computers can follow to perform tasks.
</p>
</div>
</FlipCard.Back>
</FlipCard>
),
};

export const Secondary: Story = {
args: {
image:
"https://images.unsplash.com/photo-1717966313670-a42f6908be92?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3",
title: "Bibek Bhattarai",
subtitle: "Software Engineer",
description:
"I am a full-stack developer with a passion for building beautiful and functional applications.",
rotate: "x",
},
render: ({ rotate }) => (
<FlipCard rotate={rotate}>
<FlipCard.Front>
<img
src="https://images.unsplash.com/photo-1717966313670-a42f6908be92?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Bibek Bhattarai"
className="h-full w-full rounded-2xl object-cover shadow-2xl shadow-black/40"
/>
<div className="absolute bottom-4 left-4 text-xl font-bold text-white">Bibek Bhattarai</div>
</FlipCard.Front>
<FlipCard.Back className="rounded-2xl bg-black/80 p-4 text-slate-200">
<div className="flex min-h-full flex-col gap-2">
<h1 className="text-base font-bold text-white">Software Engineer</h1>
<p className="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100">
I am a full-stack developer with a passion for building beautiful and functional
applications.
</p>
</div>
</FlipCard.Back>
</FlipCard>
),
};
116 changes: 71 additions & 45 deletions animata/card/flip-card.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,85 @@
"use client";

import { type ComponentProps, createContext, use, useMemo } from "react";

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

interface FlipCardProps extends React.HTMLAttributes<HTMLDivElement> {
image: string;
title: string;
description: string;
subtitle?: string;
rotate?: "x" | "y";
export type FlipCardRotate = "x" | "y";

type FlipCardContextValue = {
rotate: FlipCardRotate;
};

const FlipCardContext = createContext<FlipCardContextValue | null>(null);

const ROTATION_CLASS = {
x: {
hover: "group-hover/card:rotate-x-180",
back: "rotate-x-180",
},
y: {
hover: "group-hover/card:rotate-y-180",
back: "rotate-y-180",
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} as const;

function useFlipCard() {
const context = use(FlipCardContext);
if (!context) {
throw new Error("FlipCard.Front and FlipCard.Back must be used within <FlipCard>.");
}
return context;
}

export default function FlipCard({
image,
title,
description,
subtitle,
rotate = "y",
className,
...props
}: FlipCardProps) {
const rotationClass = {
x: ["group-hover/card:rotate-x-180", "rotate-x-180"],
y: ["group-hover/card:rotate-y-180", "rotate-y-180"],
} as const;
type FlipCardRootProps = ComponentProps<"div"> & {
rotate?: FlipCardRotate;
};

function FlipCardRoot({ rotate = "y", className, children, ...props }: FlipCardRootProps) {
const value = useMemo(() => ({ rotate }), [rotate]);

return (
<div className={cn("group/card h-72 w-56 perspective-[1000px]", className)} {...props}>
<div
className={cn(
"relative h-full rounded-2xl transition-transform duration-500 transform-3d",
rotationClass[rotate][0],
)}
>
{/* Front */}
<div className="absolute inset-0 backface-hidden">
<img
src={image}
alt={title}
className="h-full w-full rounded-2xl object-cover shadow-2xl shadow-black/40"
/>
<div className="absolute bottom-4 left-4 text-xl font-bold text-white">{title}</div>
</div>
{/* Back */}
<FlipCardContext.Provider value={value}>
<div className={cn("group/card h-72 w-56 perspective-[1000px]", className)} {...props}>
<div
className={cn(
"absolute inset-0 rounded-2xl bg-black/80 p-4 text-slate-200 backface-hidden",
rotationClass[rotate][1],
"relative h-full rounded-2xl transition-transform duration-500 ease-out transform-3d will-change-transform motion-reduce:transition-none",
ROTATION_CLASS[rotate].hover,
)}
>
<div className="flex min-h-full flex-col gap-2">
<h1 className="text-base font-bold text-white">{subtitle}</h1>
<p className="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100">
{description}
</p>
</div>
{children}
</div>
</div>
</div>
</FlipCardContext.Provider>
);
}

type FlipCardFaceProps = ComponentProps<"div">;

function FlipCardFront({ className, ...props }: FlipCardFaceProps) {
useFlipCard();

return <div className={cn("absolute inset-0 backface-hidden", className)} {...props} />;
}

function FlipCardBack({ className, ...props }: FlipCardFaceProps) {
const { rotate } = useFlipCard();

return (
<div
className={cn("absolute inset-0 backface-hidden", ROTATION_CLASS[rotate].back, className)}
{...props}
/>
);
}

const FlipCard = Object.assign(FlipCardRoot, {
Front: FlipCardFront,
Back: FlipCardBack,
}) as typeof FlipCardRoot & {
Front: typeof FlipCardFront;
Back: typeof FlipCardBack;
};

export default FlipCard;
export { FlipCard, FlipCardBack, FlipCardFront, FlipCardRoot };
46 changes: 34 additions & 12 deletions animata/card/swap-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,23 @@ export default function SwapCard({
firstImageClass,
)}
>
<FlipCard
className="h-72"
title={firstTitle}
description={firstDescription}
image={firstImage}
/>
<FlipCard className="h-72">
<FlipCard.Front>
<img
src={firstImage}
alt={firstTitle}
className="h-full w-full rounded-2xl object-cover shadow-2xl shadow-black/40"
/>
<div className="absolute bottom-4 left-4 text-xl font-bold text-white">
{firstTitle}
</div>
</FlipCard.Front>
<FlipCard.Back className="rounded-2xl bg-black/80 p-4 text-slate-200">
<p className="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100">
{firstDescription}
</p>
</FlipCard.Back>
</FlipCard>
</div>
<div
className={cn(
Expand All @@ -63,12 +74,23 @@ export default function SwapCard({
secondImageClass,
)}
>
<FlipCard
className="h-72"
title={secondTitle}
description={secondDescription}
image={secondImage}
/>
<FlipCard className="h-72">
<FlipCard.Front>
<img
src={secondImage}
alt={secondTitle}
className="h-full w-full rounded-2xl object-cover shadow-2xl shadow-black/40"
/>
<div className="absolute bottom-4 left-4 text-xl font-bold text-white">
{secondTitle}
</div>
</FlipCard.Front>
<FlipCard.Back className="rounded-2xl bg-black/80 p-4 text-slate-200">
<p className="mt-1 border-t border-t-gray-200 py-4 text-base font-medium leading-normal text-gray-100">
{secondDescription}
</p>
</FlipCard.Back>
</FlipCard>
</div>
</div>
</div>
Expand Down
Loading