Skip to content

Commit

Permalink
Merge branch 'codse:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
Ajdecoder authored Oct 15, 2024
2 parents b1cb4f7 + 85058d8 commit 8d27bef
Show file tree
Hide file tree
Showing 9 changed files with 720 additions and 0 deletions.
19 changes: 19 additions & 0 deletions animata/card/fluid-tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import FluidTabs from "@/animata/card/fluid-tabs";
import { Meta, StoryObj } from "@storybook/react";

const meta = {
title: "Tabs/Fluid Tabs",
component: FluidTabs,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {},
} satisfies Meta<typeof FluidTabs>;

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

export const Primary: Story = {
args: {},
};
82 changes: 82 additions & 0 deletions animata/card/fluid-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Inbox, Landmark, PieChart } from "lucide-react";

const tabs = [
{
id: "accounts",
label: "Accounts",
icon: <Landmark size={18} />,
},
{
id: "deposits",
label: "Deposits",
icon: <Inbox size={18} />,
},
{
id: "funds",
label: "Funds",
icon: <PieChart size={18} />,
},
];

export default function FluidTabs() {
const [activeTab, setActiveTab] = useState("funds");
const [touchedTab, setTouchedTab] = useState<string | null>(null);
const [prevActiveTab, setPrevActiveTab] = useState("funds");
const timeoutRef = useRef<NodeJS.Timeout | null>(null);

useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);

const handleTabClick = (tabId: string) => {
setPrevActiveTab(activeTab);
setActiveTab(tabId);
setTouchedTab(tabId);

if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setTouchedTab(null);
}, 300);
};

const getTabIndex = (tabId: string) => tabs.findIndex((tab) => tab.id === tabId);

return (
<div className="flex items-center justify-center py-4">
<div className="relative flex w-full max-w-md space-x-2 overflow-hidden rounded-full bg-[#f5f1eb] p-1 shadow-lg">
<AnimatePresence initial={false}>
<motion.div
key={activeTab}
className="absolute inset-y-0 my-1 rounded-full bg-white"
initial={{ x: `${getTabIndex(prevActiveTab) * 100}%` }}
animate={{ x: `${getTabIndex(activeTab) * 100}%` }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
style={{ width: `${100 / tabs.length}%` }}
/>
</AnimatePresence>
{tabs.map((tab) => (
<motion.button
key={tab.id}
className={`relative z-10 flex w-full items-center justify-center gap-1.5 px-5 py-3 text-sm font-bold transition-colors duration-300 ${
activeTab === tab.id ? "font-bold text-black" : "text-gray-500"
} ${touchedTab === tab.id ? "blur-sm" : ""}`}
onClick={() => handleTabClick(tab.id)}
>
{tab.icon}
{tab.label}
</motion.button>
))}
</div>
</div>
);
}
86 changes: 86 additions & 0 deletions animata/list/transaction-list.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ChefHat, Receipt, Signal } from "lucide-react";

import TransactionList from "@/animata/list/transaction-list";
import { Meta, StoryObj } from "@storybook/react";

const meta = {
title: "List/Transaction List",
component: TransactionList,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: { transactions: { control: { type: "object" } } },
} satisfies Meta<typeof TransactionList>;

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

const transactions = [
{
id: "67593",
name: "Netflix",
type: "Subscription",
amount: -6.99,
date: "September 26",
time: "12:01 am",
icon: <Receipt className="h-10 w-10 p-2 text-white" />,
paymentMethod: "Credit Card",
cardLastFour: "9342",
cardType: "visa",
},
{
id: "67482",
name: "Verizon",
type: "Mobile Recharge",
amount: -4.05,
date: "September 24",
time: "05:18 pm",
icon: <Signal className="h-10 w-10 p-2 text-white" />,
paymentMethod: "Credit Card",
cardLastFour: "2316",
cardType: "mastercard",
},
{
id: "52363",
name: "Figma",
type: "Subscription",
amount: -15.0,
date: "September 15",
time: "01:11 pm",
icon: <Receipt className="h-10 w-10 p-2 text-white" />,
paymentMethod: "Credit Card",
cardLastFour: "9342",
cardType: "visa",
},
{
id: "54635",
name: "Rive",
type: "Subscription",
amount: -32.0,
date: "September 16",
time: "02:11 pm",
icon: <Receipt className="h-10 w-10 p-2 text-white" />,
paymentMethod: "Credit Card",
cardLastFour: "9342",
cardType: "mastercard",
},
{
id: "52342",
name: "Big Belly Burger",
type: "Restaurant",
amount: -12.05,
date: "September 12",
time: "09:06 pm",
icon: <ChefHat className="h-10 w-10 p-2 text-white" />,
paymentMethod: "Credit Card",
cardLastFour: "2316",
cardType: "visa",
},
];

export const Primary: Story = {
args: {
transactions: transactions,
},
};
199 changes: 199 additions & 0 deletions animata/list/transaction-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React, { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { ArrowRight, CreditCard, X } from "lucide-react";

interface Transaction {
id: string;
name: string;
type: string;
amount: number;
date: string;
time: string;
icon: React.ReactNode;
paymentMethod: string;
cardLastFour: string;
cardType?: string;
}

export default function TransactionList({ transactions }: { transactions: Transaction[] }) {
const [selectedTransaction, setSelectedTransaction] = useState<Transaction | null>(null);

return (
<div className="mx-auto max-w-md font-sans">
<motion.div
layout
className="w-[350px] overflow-hidden rounded-3xl bg-white shadow"
initial={{
height: 420,
width: 300,
}}
animate={{
height: selectedTransaction ? 350 : 420,
width: 300,
}}
transition={{ duration: 0.5, ease: "easeInOut" }}
>
<AnimatePresence mode="wait">
{!selectedTransaction ? (
<motion.div
key="list"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<h2 className="pl-6 pt-3 text-xl font-semibold text-gray-400">Transactions</h2>
<div className="space-y-2 p-2">
{transactions.map((transaction) => (
<motion.div
key={transaction.id}
layoutId={`transaction-${transaction.id}`}
className="flex cursor-pointer items-center justify-between rounded-lg p-1"
onClick={() => setSelectedTransaction(transaction)}
>
<div className="flex items-center space-x-3">
<motion.div
layoutId={`icon-${transaction.id}`}
className="rounded-full bg-black"
transition={{ duration: 0.5 }}
>
{transaction.icon}
</motion.div>
<div>
<motion.p layoutId={`name-${transaction.id}`} className="font-medium">
{transaction.name}
</motion.p>
<motion.p
layoutId={`type-${transaction.id}`}
className="text-sm text-gray-400"
>
{transaction.type}
</motion.p>
</div>
</div>
<motion.p
layoutId={`amount-${transaction.id}`}
className="font-bold text-gray-400"
>
${Math.abs(transaction.amount).toFixed(2)}
</motion.p>
</motion.div>
))}
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="m-auto mt-4 flex w-11/12 items-center justify-center rounded-xl bg-gray-100 py-2 text-gray-800"
>
All Transactions <ArrowRight className="ml-2 h-4 w-4" />
</motion.button>
</motion.div>
) : (
<motion.div
key="details"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="p-4"
>
<div className="mb-4 flex items-center justify-between">
<motion.div
layoutId={`transaction-${selectedTransaction.id}`}
className="flex items-center space-x-3"
>
<motion.div
layoutId={`icon-${selectedTransaction.id}`}
className="rounded-xl bg-black"
transition={{ duration: 0.5 }}
>
{selectedTransaction.icon}
</motion.div>
</motion.div>
<button onClick={() => setSelectedTransaction(null)}>
<X className="h-6 w-6 rounded-full bg-gray-400 text-white" />
</button>
</div>
<div className="flex justify-between border-b border-dashed pb-4">
<div className="space-y-1">
<motion.p layoutId={`name-${selectedTransaction.id}`} className="font-medium">
{selectedTransaction.name}
</motion.p>
<motion.p
layoutId={`type-${selectedTransaction.id}`}
className="text-sm text-gray-400"
>
{selectedTransaction.type}
</motion.p>
</div>
<motion.p
layoutId={`amount-${selectedTransaction.id}`}
className="font-bold text-gray-400"
>
${Math.abs(selectedTransaction.amount).toFixed(2)}
</motion.p>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="space-y-4"
>
<div className="mt-4 space-y-2 text-gray-400">
<p>#{selectedTransaction.id}</p>
<p>{selectedTransaction.date}</p>
<p>{selectedTransaction.time}</p>
</div>
<div className="border-t border-dashed pt-4 text-gray-400">
<p className="font-medium">Paid Via {selectedTransaction.paymentMethod}</p>
<div className="mt-2 flex items-center space-x-2">
<CreditCard className="h-5 w-5" />
<p>XXXX {selectedTransaction.cardLastFour}</p>
<p>
{selectedTransaction.cardType === "visa" ? <VisaLogo /> : <MasterCardLogo />}
</p>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</div>
);
}

const MasterCardLogo = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1.29em" height="1em" viewBox="0 0 256 199">
<path d="M46.54 198.011V184.84c0-5.05-3.074-8.342-8.343-8.342c-2.634 0-5.488.878-7.464 3.732c-1.536-2.415-3.731-3.732-7.024-3.732c-2.196 0-4.39.658-6.147 3.073v-2.634h-4.61v21.074h4.61v-11.635c0-3.731 1.976-5.488 5.05-5.488c3.072 0 4.61 1.976 4.61 5.488v11.635h4.61v-11.635c0-3.731 2.194-5.488 5.048-5.488c3.074 0 4.61 1.976 4.61 5.488v11.635zm68.271-21.074h-7.463v-6.366h-4.61v6.366h-4.171v4.17h4.17v9.66c0 4.83 1.976 7.683 7.245 7.683c1.976 0 4.17-.658 5.708-1.536l-1.318-3.952c-1.317.878-2.853 1.098-3.951 1.098c-2.195 0-3.073-1.317-3.073-3.513v-9.44h7.463zm39.076-.44c-2.634 0-4.39 1.318-5.488 3.074v-2.634h-4.61v21.074h4.61v-11.854c0-3.512 1.536-5.488 4.39-5.488c.878 0 1.976.22 2.854.439l1.317-4.39c-.878-.22-2.195-.22-3.073-.22m-59.052 2.196c-2.196-1.537-5.269-2.195-8.562-2.195c-5.268 0-8.78 2.634-8.78 6.805c0 3.513 2.634 5.488 7.244 6.147l2.195.22c2.415.438 3.732 1.097 3.732 2.195c0 1.536-1.756 2.634-4.83 2.634s-5.488-1.098-7.025-2.195l-2.195 3.512c2.415 1.756 5.708 2.634 9 2.634c6.147 0 9.66-2.853 9.66-6.805c0-3.732-2.854-5.708-7.245-6.366l-2.195-.22c-1.976-.22-3.512-.658-3.512-1.975c0-1.537 1.536-2.415 3.951-2.415c2.635 0 5.269 1.097 6.586 1.756zm122.495-2.195c-2.635 0-4.391 1.317-5.489 3.073v-2.634h-4.61v21.074h4.61v-11.854c0-3.512 1.537-5.488 4.39-5.488c.879 0 1.977.22 2.855.439l1.317-4.39c-.878-.22-2.195-.22-3.073-.22m-58.833 10.976c0 6.366 4.39 10.976 11.196 10.976c3.073 0 5.268-.658 7.463-2.414l-2.195-3.732c-1.756 1.317-3.512 1.975-5.488 1.975c-3.732 0-6.366-2.634-6.366-6.805c0-3.951 2.634-6.586 6.366-6.805c1.976 0 3.732.658 5.488 1.976l2.195-3.732c-2.195-1.757-4.39-2.415-7.463-2.415c-6.806 0-11.196 4.61-11.196 10.976m42.588 0v-10.537h-4.61v2.634c-1.537-1.975-3.732-3.073-6.586-3.073c-5.927 0-10.537 4.61-10.537 10.976s4.61 10.976 10.537 10.976c3.073 0 5.269-1.097 6.586-3.073v2.634h4.61zm-16.904 0c0-3.732 2.415-6.805 6.366-6.805c3.732 0 6.367 2.854 6.367 6.805c0 3.732-2.635 6.805-6.367 6.805c-3.951-.22-6.366-3.073-6.366-6.805m-55.1-10.976c-6.147 0-10.538 4.39-10.538 10.976s4.39 10.976 10.757 10.976c3.073 0 6.147-.878 8.562-2.853l-2.196-3.293c-1.756 1.317-3.951 2.195-6.146 2.195c-2.854 0-5.708-1.317-6.367-5.05h15.587v-1.755c.22-6.806-3.732-11.196-9.66-11.196m0 3.951c2.853 0 4.83 1.757 5.268 5.05h-10.976c.439-2.854 2.415-5.05 5.708-5.05m114.372 7.025v-18.879h-4.61v10.976c-1.537-1.975-3.732-3.073-6.586-3.073c-5.927 0-10.537 4.61-10.537 10.976s4.61 10.976 10.537 10.976c3.074 0 5.269-1.097 6.586-3.073v2.634h4.61zm-16.903 0c0-3.732 2.414-6.805 6.366-6.805c3.732 0 6.366 2.854 6.366 6.805c0 3.732-2.634 6.805-6.366 6.805c-3.952-.22-6.366-3.073-6.366-6.805m-154.107 0v-10.537h-4.61v2.634c-1.537-1.975-3.732-3.073-6.586-3.073c-5.927 0-10.537 4.61-10.537 10.976s4.61 10.976 10.537 10.976c3.074 0 5.269-1.097 6.586-3.073v2.634h4.61zm-17.123 0c0-3.732 2.415-6.805 6.366-6.805c3.732 0 6.367 2.854 6.367 6.805c0 3.732-2.635 6.805-6.367 6.805c-3.951-.22-6.366-3.073-6.366-6.805"></path>
<path fill="black" d="M93.298 16.903h69.15v124.251h-69.15z"></path>
<path
fill="black"
d="M97.689 79.029c0-25.245 11.854-47.637 30.074-62.126C114.373 6.366 97.47 0 79.03 0C35.343 0 0 35.343 0 79.029s35.343 79.029 79.029 79.029c18.44 0 35.343-6.366 48.734-16.904c-18.22-14.269-30.074-36.88-30.074-62.125"
></path>
<path
fill="#5e5e5e"
d="M255.746 79.029c0 43.685-35.343 79.029-79.029 79.029c-18.44 0-35.343-6.366-48.734-16.904c18.44-14.488 30.075-36.88 30.075-62.125s-11.855-47.637-30.075-62.126C141.373 6.366 158.277 0 176.717 0c43.686 0 79.03 35.563 79.03 79.029"
></path>
</svg>
);
};

const VisaLogo = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="3.09em" height="1em" viewBox="0 0 256 83">
<defs>
<linearGradient id="logosVisa0" x1="45.974%" x2="54.877%" y1="-2.006%" y2="100%">
<stop offset="0%" stopColor="gray"></stop>
<stop offset="100%" stopColor="gray"></stop>
</linearGradient>
</defs>
<path
fill="url(#logosVisa0)"
d="M132.397 56.24c-.146-11.516 10.263-17.942 18.104-21.763c8.056-3.92 10.762-6.434 10.73-9.94c-.06-5.365-6.426-7.733-12.383-7.825c-10.393-.161-16.436 2.806-21.24 5.05l-3.744-17.519c4.82-2.221 13.745-4.158 23-4.243c21.725 0 35.938 10.724 36.015 27.351c.085 21.102-29.188 22.27-28.988 31.702c.069 2.86 2.798 5.912 8.778 6.688c2.96.392 11.131.692 20.395-3.574l3.636 16.95c-4.982 1.814-11.385 3.551-19.357 3.551c-20.448 0-34.83-10.87-34.946-26.428m89.241 24.968c-3.967 0-7.31-2.314-8.802-5.865L181.803 1.245h21.709l4.32 11.939h26.528l2.506-11.939H256l-16.697 79.963zm3.037-21.601l6.265-30.027h-17.158zm-118.599 21.6L88.964 1.246h20.687l17.104 79.963zm-30.603 0L53.941 26.782l-8.71 46.277c-1.022 5.166-5.058 8.149-9.54 8.149H.493L0 78.886c7.226-1.568 15.436-4.097 20.41-6.803c3.044-1.653 3.912-3.098 4.912-7.026L41.819 1.245H63.68l33.516 79.963z"
transform="matrix(1 0 0 -1 0 82.668)"
></path>
</svg>
);
};
Loading

0 comments on commit 8d27bef

Please sign in to comment.