Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added Run Widget #328 #389

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 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
11 changes: 3 additions & 8 deletions animata/container/animated-dock.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Meta, StoryObj } from "@storybook/react";
import { Home, Search, Bell, User } from "lucide-react";
import AnimatedDock from "@/animata/container/animated-dock";
import { Bell, Home, Search, User } from "lucide-react";

import AnimatedDock from "@/animata/container/animated-dock";
import { Meta, StoryObj } from "@storybook/react";

const meta = {
title: "Container/Animated Dock",
Expand All @@ -19,7 +19,6 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;


// Example contents for AnimatedDock
const dockItems = [
{ title: "Home", icon: <Home />, href: "/" },
Expand All @@ -28,7 +27,6 @@ const dockItems = [
{ title: "Profile", icon: <User />, href: "/profile" },
];


// Primary story for AnimatedDock (default layout)
export const Primary: Story = {
args: {
Expand All @@ -43,7 +41,6 @@ export const Primary: Story = {
),
};


// Story focused on the Small layout (for mobile view)
export const Small: Story = {
args: {
Expand All @@ -57,7 +54,6 @@ export const Small: Story = {
),
};


// Story focused on the Large layout (for desktop view)
export const Large: Story = {
args: {
Expand All @@ -71,7 +67,6 @@ export const Large: Story = {
),
};


// Story showing both layouts at the same time (for comparison)
export const Multiple: Story = {
args: {
Expand Down
9 changes: 5 additions & 4 deletions animata/container/animated-dock.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { cn } from "@/lib/utils"; // Import utility for conditional class names
import React, { useRef, useState } from "react"; // Importing React hooks
import Link from "next/link"; // Next.js Link component for navigation
import {
AnimatePresence, // Enables animation presence detection
MotionValue, // Type for motion values
motion, // Main component for animations
MotionValue, // Type for motion values
useMotionValue, // Hook to create a motion value
useSpring, // Hook to create smooth spring animations
useTransform, // Hook to transform motion values
} from "framer-motion";
import Link from "next/link"; // Next.js Link component for navigation
import React, { useRef, useState } from "react"; // Importing React hooks
import { Menu, X } from "lucide-react"; // Importing icons from lucide-react

import { cn } from "@/lib/utils"; // Import utility for conditional class names

// Interface for props accepted by the AnimatedDock component
interface AnimatedDockProps {
items: { title: string; icon: React.ReactNode; href: string }[]; // Array of menu items
Expand Down
22 changes: 22 additions & 0 deletions animata/widget/run.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Run from "@/animata/widget/run";
import { Meta, StoryObj } from "@storybook/react";

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

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

export const Primary: Story = {
args: {
unit: "miles",
buttonText: "🏃 Begin Run",
},
};
177 changes: 177 additions & 0 deletions animata/widget/run.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"use client";
import { useEffect, useRef, useState } from "react";
import {
AnimatePresence,
motion,
MotionValue,
useMotionValue,
useMotionValueEvent,
useReducedMotion,
useSpring,
useTransform,
} from "framer-motion";
import { MoveUpRight } from "lucide-react";
import PropTypes from "prop-types";

const AnimatedScroll: React.FC<{ setDistance: React.Dispatch<React.SetStateAction<number>> }> = ({
setDistance,
}) => {
const sliderRef = useRef<HTMLDivElement>(null);

const x = useMotionValue(0);

useMotionValueEvent(x, "change", (latest) => {
let val = Math.ceil(Math.floor(latest) / 10);
val = -val + 10;
setDistance(val);
});

return (
<div className="relative flex h-full w-full items-center justify-center px-1">
<div ref={sliderRef} className="h-[25px] w-full overflow-hidden px-2 tracking-tighter">
<motion.div
className="flex gap-x-1 whitespace-nowrap text-3xl text-gray-500"
style={{ x }}
drag="x"
dragConstraints={{ left: -890, right: 90 }}
dragElastic={false}
>
{Array.from({ length: 88 }, (_, index) => (
<div key={index}>|</div>
))}
</motion.div>
</div>
<div className="absolute left-[49%] top-0 z-[2] h-fit text-5xl text-black">|</div>
</div>
);
};

const fontSize = 30;
const padding = 15;
const height = fontSize + padding;

const Counter: React.FC<{ value: number }> = ({ value }) => {
return (
<div
style={{ fontSize }}
className="flex w-[70px] justify-between overflow-hidden rounded bg-white leading-none text-gray-900"
roy-abir05 marked this conversation as resolved.
Show resolved Hide resolved
>
<Digit place={10} value={value} />
<span className="mt-[2px] text-5xl">.</span>
<Digit place={1} value={value} />
</div>
);
};

const Digit: React.FC<{ place: number; value: number }> = ({ place, value }) => {
const valueRoundedToPlace = Math.floor(value / place);
const animatedValue = useSpring(valueRoundedToPlace, {
stiffness: 45,
damping: 7,
});

useEffect(() => {
animatedValue.set(valueRoundedToPlace);
}, [animatedValue, valueRoundedToPlace]);

return (
<div style={{ height }} className="relative w-[28px] tabular-nums">
{[...Array(10).keys()].map((i) => (
<Number key={i} mv={animatedValue} number={i} />
))}
</div>
);
};

const Number: React.FC<{ mv: MotionValue; number: number }> = ({ mv, number }) => {
const shouldReduceMotion = useReducedMotion();

const y = useTransform(mv, (latest) => {
const placeValue = latest % 10;
const offset = (10 + number - placeValue) % 10;

let memo = -offset * height;

if (offset > 5) {
memo += 10 * height;
}

return memo;
});

const opacity = useTransform(y, [-height, 0, height], [0.2, 1, 0.2]);
const scale = useTransform(y, [-height, 0, height], [0.2, 1, 0.2]);
const filter = useTransform(y, [-height, 0, height], ["blur(5px)", "blur(0px)", "blur(5px)"]);

if (shouldReduceMotion) {
scale.set(1);
filter.set("blur(0px)");
}

return (
<motion.span
style={{
y,
filter,
scale,
opacity,
}}
className="absolute flex items-center justify-center font-sans text-5xl"
>
{number}
</motion.span>
);
};

AnimatedScroll.propTypes = {
setDistance: PropTypes.func.isRequired,
};

Counter.propTypes = {
value: PropTypes.number.isRequired,
};

Digit.propTypes = {
place: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
};

Number.propTypes = {
mv: PropTypes.instanceOf(MotionValue).isRequired,
number: PropTypes.number.isRequired,
};

export default function Run({
unit = "miles",
buttonText = "🏃 Begin Run",
}: {
unit?: string;
buttonText?: string;
}) {
const [distance, setDistance] = useState(10);

return (
<div className="flex h-48 w-48 flex-col justify-between overflow-hidden rounded-[18px] bg-white p-[2px] text-black">
<div className="flex items-center justify-between px-2">
<div className="relative flex h-[50px] w-fit items-center justify-evenly overflow-hidden text-5xl font-extrabold">
<AnimatePresence mode="sync">
<Counter value={distance} />
</AnimatePresence>
</div>
<button className="flex items-center justify-center rounded-md bg-[rgb(142,139,134)] p-2">
<div className="flex h-4 w-4 items-center justify-center overflow-hidden">
<MoveUpRight strokeWidth={4} className="text-white" />
</div>
</button>
</div>
<div className="px-2 font-bold text-gray-500">{unit}</div>
<div className="h-[40px] w-full overflow-hidden">
<AnimatedScroll setDistance={setDistance} />
</div>
<button className="mx-auto mt-5 h-12 w-[99%] rounded-lg rounded-bl-[18px] rounded-br-[18px] bg-[rgb(213,203,215)] hover:bg-gray-400">
{buttonText}
</button>{" "}
{/*https://emojipedia.org/man-running*/}
</div>
);
}
51 changes: 51 additions & 0 deletions content/docs/widget/run.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: Run
description: A widget to set a target for the distance to run
author: YourTwitterUsername
roy-abir05 marked this conversation as resolved.
Show resolved Hide resolved
---

<ComponentPreview name="widget-run--docs" />

## Installation

<Steps>
<Step>Install dependencies</Step>

```bash
npm install framer-motion lucide-react
```

<Step>Update `tailwind.config.js`</Step>

Add the following to your tailwind.config.js file.

```json
module.exports = {
theme: {
extend: {
}
}
}
```

roy-abir05 marked this conversation as resolved.
Show resolved Hide resolved
<Step>Run the following command</Step>

It will create a new file `run.tsx` inside the `components/animata/widget` directory.

```bash
mkdir -p components/animata/widget && touch components/animata/widget/run.tsx
```

<Step>Paste the code</Step>{" "}

Open the newly created file and paste the following code:

```jsx file=<rootDir>/animata/widget/run.tsx

```

</Steps>

## Credits

Built by [Abir Roy](https://github.com/roy-abir05)
Loading