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/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 (
-
-
-
-
- -
- Get started by editing{" "}
-
- src/app/page.tsx
-
- .
-
- - Save and see your changes instantly.
-
-
-
-
-
-
+ <>
+
+ >
);
}
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..81c8678
--- /dev/null
+++ b/src/components/Marquee/tweet-card.tsx
@@ -0,0 +1,40 @@
+import { cn } from "@/lib/utils";
+import Image from "next/image";
+
+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..65f530a
--- /dev/null
+++ b/src/components/Marquee/tweet-section.tsx
@@ -0,0 +1,33 @@
+import { Marquee } from "./marquee";
+import TweetCard from "./tweet-card";
+import { tweetsData } from "@/data/tweets";
+
+const TweetSection = () => {
+ const firstRow = tweetsData.slice(0, tweetsData.length / 2);
+ const secondRow = tweetsData.slice(tweetsData.length / 2);
+
+ return (
+
+
+ درمورد فرانت چپتر چی
+ می گن؟
+
+
+
+
+
+
+
+
+ );
+};
+
+export default TweetSection;
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",
+ },
+];
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;