From 5ae483542899e41a4683c6429f670ccb7f44c4dd Mon Sep 17 00:00:00 2001 From: bravemobin Date: Tue, 28 Jan 2025 11:06:35 +0330 Subject: [PATCH 1/4] feat: Add tweet marquee component --- package.json | 4 +- pnpm-lock.yaml | 21 ++++- src/app/page.tsx | 100 +---------------------- src/components/Marquee/marquee.tsx | 73 +++++++++++++++++ src/components/Marquee/tweet-card.tsx | 38 +++++++++ src/components/Marquee/tweet-section.tsx | 65 +++++++++++++++ src/lib/utils.tsx | 6 ++ tailwind.config.ts | 14 ++++ 8 files changed, 222 insertions(+), 99 deletions(-) create mode 100644 src/components/Marquee/marquee.tsx create mode 100644 src/components/Marquee/tweet-card.tsx create mode 100644 src/components/Marquee/tweet-section.tsx create mode 100644 src/lib/utils.tsx diff --git a/package.json b/package.json index 3efeb6c..dff5cfc 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "dependencies": { "next": "15.1.6", "react": "^19.0.0", + "clsx": "^2.1.1", + "tailwind-merge": "^2.6.0", "react-dom": "^19.0.0" }, "devDependencies": { @@ -27,4 +29,4 @@ "typescript": "^5" }, "packageManager": "pnpm@9.5.0" -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9086de3..cdb0c37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + clsx: + specifier: ^2.1.1 + version: 2.1.1 next: specifier: 15.1.6 version: 15.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -17,6 +20,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -596,6 +602,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1687,6 +1697,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} + tailwindcss@3.4.17: resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} @@ -2351,6 +2364,8 @@ snapshots: client-only@0.0.1: {} + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2597,7 +2612,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.21.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.19.0(jiti@1.21.7)))(eslint@9.19.0(jiti@1.21.7)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.21.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.19.0(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: @@ -2619,7 +2634,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.19.0(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.21.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.19.0(jiti@1.21.7)))(eslint@9.19.0(jiti@1.21.7)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.21.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.19.0(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -3608,6 +3623,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tailwind-merge@2.6.0: {} + tailwindcss@3.4.17: dependencies: '@alloc/quick-lru': 5.2.0 diff --git a/src/app/page.tsx b/src/app/page.tsx index 3eee014..d1b27bd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,101 +1,9 @@ -import Image from "next/image"; +import TweetSection from "@/components/Marquee/tweet-section"; export default function Home() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. Save and see your changes instantly.
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - -
-
- -
+ <> + + ); } diff --git a/src/components/Marquee/marquee.tsx b/src/components/Marquee/marquee.tsx new file mode 100644 index 0000000..fa9c129 --- /dev/null +++ b/src/components/Marquee/marquee.tsx @@ -0,0 +1,73 @@ +import { cn } from "@/lib/utils"; +import { ComponentPropsWithoutRef } from "react"; + +interface MarqueeProps extends ComponentPropsWithoutRef<"div"> { + /** + * Optional CSS class name to apply custom styles + */ + className?: string; + /** + * Whether to reverse the animation direction + * @default false + */ + reverse?: boolean; + /** + * Whether to pause the animation on hover + * @default false + */ + pauseOnHover?: boolean; + /** + * Content to be displayed in the marquee + */ + children: React.ReactNode; + /** + * Whether to animate vertically instead of horizontally + * @default false + */ + vertical?: boolean; + /** + * Number of times to repeat the content + * @default 4 + */ + repeat?: number; +} + +export function Marquee({ + className, + reverse = false, + pauseOnHover = false, + children, + vertical = false, + repeat = 4, + ...props +}: MarqueeProps) { + return ( +
+ {Array(repeat) + .fill(0) + .map((_, i) => ( +
+ {children} +
+ ))} +
+ ); +} diff --git a/src/components/Marquee/tweet-card.tsx b/src/components/Marquee/tweet-card.tsx new file mode 100644 index 0000000..e2d41cd --- /dev/null +++ b/src/components/Marquee/tweet-card.tsx @@ -0,0 +1,38 @@ +import { cn } from "@/lib/utils"; + +const TweetCard = ({ + img, + name, + username, + body, +}: { + img: string; + name: string; + username: string; + body: string; +}) => { + return ( +
+
+ +
+
+ {name} +
+

{username}

+
+
+
{body}
+
+ ); +}; + +export default TweetCard; diff --git a/src/components/Marquee/tweet-section.tsx b/src/components/Marquee/tweet-section.tsx new file mode 100644 index 0000000..7bd4ca7 --- /dev/null +++ b/src/components/Marquee/tweet-section.tsx @@ -0,0 +1,65 @@ +import { Marquee } from "./marquee"; +import TweetCard from "./tweet-card"; + +const reviews = [ + { + name: "Jack", + username: "@jack", + body: "I've never seen anything like this before. It's amazing. I love it.", + img: "https://avatar.vercel.sh/jack", + }, + { + name: "Jill", + username: "@jill", + body: "I don't know what to say. I'm speechless. This is amazing.", + img: "https://avatar.vercel.sh/jill", + }, + { + name: "John", + username: "@john", + body: "I'm at a loss for words. This is amazing. I love it.", + img: "https://avatar.vercel.sh/john", + }, + { + name: "Jane", + username: "@jane", + body: "I'm at a loss for words. This is amazing. I love it.", + img: "https://avatar.vercel.sh/jane", + }, + { + name: "Jenny", + username: "@jenny", + body: "I'm at a loss for words. This is amazing. I love it.", + img: "https://avatar.vercel.sh/jenny", + }, + { + name: "James", + username: "@james", + body: "I'm at a loss for words. This is amazing. I love it.", + img: "https://avatar.vercel.sh/james", + }, +]; + +const TweetSection = () => { + const firstRow = reviews.slice(0, reviews.length / 2); + const secondRow = reviews.slice(reviews.length / 2); + + return ( +
+ + {firstRow.map((review) => ( + + ))} + + + {secondRow.map((review) => ( + + ))} + +
+
+
+ ); +}; + +export default TweetSection; diff --git a/src/lib/utils.tsx b/src/lib/utils.tsx new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/src/lib/utils.tsx @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 109807b..7ccabb8 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -13,6 +13,20 @@ export default { foreground: "var(--foreground)", }, }, + animation: { + marquee: "marquee var(--duration) linear infinite", + "marquee-vertical": "marquee-vertical var(--duration) linear infinite", + }, + keyframes: { + marquee: { + from: { transform: "translateX(0)" }, + to: { transform: "translateX(calc(-100% - var(--gap)))" }, + }, + "marquee-vertical": { + from: { transform: "translateY(0)" }, + to: { transform: "translateY(calc(-100% - var(--gap)))" }, + }, + }, }, plugins: [], } satisfies Config; From 10ea555fdf6b4db857b38552a2238be23c5addb5 Mon Sep 17 00:00:00 2001 From: bravemobin Date: Tue, 28 Jan 2025 11:11:41 +0330 Subject: [PATCH 2/4] feat: Add heading to tweet section --- src/components/Marquee/tweet-section.tsx | 31 ++++++++++++++---------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/components/Marquee/tweet-section.tsx b/src/components/Marquee/tweet-section.tsx index 7bd4ca7..d16ebfd 100644 --- a/src/components/Marquee/tweet-section.tsx +++ b/src/components/Marquee/tweet-section.tsx @@ -45,19 +45,24 @@ const TweetSection = () => { const secondRow = reviews.slice(reviews.length / 2); return ( -
- - {firstRow.map((review) => ( - - ))} - - - {secondRow.map((review) => ( - - ))} - -
-
+
+

+ درمورد فرانت چپتر چی میگن؟ +

+
+ + {firstRow.map((review) => ( + + ))} + + + {secondRow.map((review) => ( + + ))} + +
+
+
); }; From dd1da56afc449fb0e6627c1a5f50b842e2a62942 Mon Sep 17 00:00:00 2001 From: bravemobin Date: Tue, 28 Jan 2025 11:54:16 +0330 Subject: [PATCH 3/4] feat: Add tweetsData file for dynamic tweets --- src/components/Marquee/tweet-card.tsx | 3 +- src/components/Marquee/tweet-section.tsx | 51 ++++-------------------- src/data/tweets.ts | 38 ++++++++++++++++++ 3 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 src/data/tweets.ts diff --git a/src/components/Marquee/tweet-card.tsx b/src/components/Marquee/tweet-card.tsx index e2d41cd..8addd15 100644 --- a/src/components/Marquee/tweet-card.tsx +++ b/src/components/Marquee/tweet-card.tsx @@ -13,8 +13,9 @@ const TweetCard = ({ }) => { return (
{ - const firstRow = reviews.slice(0, reviews.length / 2); - const secondRow = reviews.slice(reviews.length / 2); + const firstRow = tweetsData.slice(0, tweetsData.length / 2); + const secondRow = tweetsData.slice(tweetsData.length / 2); return ( -
-

- درمورد فرانت چپتر چی میگن؟ +
+

+ درمورد فرانت چپتر چی + می گن؟

diff --git a/src/data/tweets.ts b/src/data/tweets.ts new file mode 100644 index 0000000..ba4c3f0 --- /dev/null +++ b/src/data/tweets.ts @@ -0,0 +1,38 @@ +export const tweetsData = [ + { + name: "صالح شجاعی", + username: "@felxxbs", + body: "رفقا می‌تونید با رزرو زودتر بلیط اقمتگاهتون، راحت‌تر توی همایش شرکت کنید#frontchapter1402", + img: "https://avatar.vercel.sh/jack", + }, + { + name: "Jill", + username: "@jill", + body: "I don't know what to say. I'm speechless. This is amazing.", + img: "https://avatar.vercel.sh/jill", + }, + { + name: "John", + username: "@john", + body: "I'm at a loss for words. This is amazing. I love it.", + img: "https://avatar.vercel.sh/john", + }, + { + name: "Jane", + username: "@jane", + body: "I'm at a loss for words. This is amazing. I love it.", + img: "https://avatar.vercel.sh/jane", + }, + { + name: "Jenny", + username: "@jenny", + body: "I'm at a loss for words. This is amazing. I love it.", + img: "https://avatar.vercel.sh/jenny", + }, + { + name: "James", + username: "@james", + body: "I'm at a loss for words. This is amazing. I love it.", + img: "https://avatar.vercel.sh/james", + }, +]; From 74fc53e16cf0c2375524cbeefd4a2a4c9fca79cc Mon Sep 17 00:00:00 2001 From: bravemobin Date: Tue, 28 Jan 2025 11:56:42 +0330 Subject: [PATCH 4/4] feat: Add image optimization for avatars --- next.config.ts | 11 ++++++++++- src/components/Marquee/tweet-card.tsx | 3 ++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/next.config.ts b/next.config.ts index e9ffa30..ed80c1d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,16 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "avatar.vercel.sh", + port: "", + pathname: "**", + }, + ], + }, }; export default nextConfig; diff --git a/src/components/Marquee/tweet-card.tsx b/src/components/Marquee/tweet-card.tsx index 8addd15..81c8678 100644 --- a/src/components/Marquee/tweet-card.tsx +++ b/src/components/Marquee/tweet-card.tsx @@ -1,4 +1,5 @@ import { cn } from "@/lib/utils"; +import Image from "next/image"; const TweetCard = ({ img, @@ -23,7 +24,7 @@ const TweetCard = ({ )} >
- +
{name}