├── .gitignore ├── .vscode └── settings.json ├── README.md ├── app ├── active-link │ ├── customers │ │ └── page.tsx │ ├── navigation-menu.tsx │ ├── page.tsx │ ├── partners │ │ └── page.tsx │ └── team │ │ └── page.tsx ├── animated-accordion │ └── page.tsx ├── animated-switch │ └── page.tsx ├── api │ ├── hello │ │ └── route.ts │ └── og │ │ └── route.tsx ├── blur-reveal │ └── page.tsx ├── dropdown-examples │ └── page.tsx ├── fancy-testimonials-slider │ └── page.tsx ├── favicon.ico ├── feature-comparison-pricing-table │ └── page.tsx ├── globals.css ├── layout.tsx ├── logo-carousel │ └── page.tsx ├── masonry │ ├── page.tsx │ └── testimonials-grid.tsx ├── modal-video │ └── page.tsx ├── multiselect │ └── page.tsx ├── particle-animation │ └── page.tsx ├── password-strength │ └── page.tsx ├── pricing-table │ └── page.tsx ├── progress-slider │ └── page.tsx ├── rotating-words │ └── page.tsx ├── search-modal │ └── page.tsx ├── show-password │ └── page.tsx ├── sliding-active-link │ ├── nav-provider.tsx │ ├── navigation-menu.tsx │ ├── page.tsx │ └── section.tsx ├── social-preview │ └── page.tsx ├── spotlight-effect │ └── page.tsx ├── sticky-scrolling │ └── page.tsx ├── unconventional-tabs │ └── page.tsx └── vertical-timelines │ └── page.tsx ├── components ├── accordion.tsx ├── banner.tsx ├── blur-reveal.tsx ├── command.tsx ├── dialog.tsx ├── dropdown-01.tsx ├── dropdown-02.tsx ├── dropdown-03.tsx ├── dropdown-04.tsx ├── fancy-testimonials-slider.tsx ├── feature-comparison-pricing.tsx ├── logo-carousel.tsx ├── modal-video.tsx ├── multiselect.tsx ├── nav-link.tsx ├── particles.tsx ├── password-field.tsx ├── password-strength-field.tsx ├── pricing-table.tsx ├── progress-slider.tsx ├── rotating-words.tsx ├── search-modal.tsx ├── spotlight.tsx ├── sticky-scrolling.tsx ├── switch.tsx ├── testimonial.tsx ├── unconventional-tabs.tsx ├── utils │ ├── useMasonry.ts │ └── useMousePosition.ts ├── vertical-timeline-01.tsx ├── vertical-timeline-02.tsx └── vertical-timeline-03.tsx ├── lib └── utils.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── Inter-Bold.ttf ├── Inter-ExtraBold.ttf ├── airbnb.svg ├── apple.svg ├── author.png ├── card-01.png ├── card-02.png ├── card-03.png ├── disney.svg ├── dropdown-user.jpg ├── facebook.svg ├── illustration-01.png ├── illustration-02.png ├── illustration-03.png ├── illustration-04.png ├── masonry-01.jpg ├── masonry-02.jpg ├── masonry-03.jpg ├── masonry-04.jpg ├── masonry-05.jpg ├── masonry-06.jpg ├── masonry-07.jpg ├── masonry-08.jpg ├── masonry-09.jpg ├── modal-video-thumb.jpg ├── ps-icon-01.svg ├── ps-icon-02.svg ├── ps-icon-03.svg ├── ps-icon-04.svg ├── ps-image-01.png ├── ps-image-02.png ├── ps-image-03.png ├── ps-image-04.png ├── quora.svg ├── samsung.svg ├── sass.svg ├── shape.svg ├── social-card-bg.jpg ├── spark.svg ├── tabs-image-01.jpg ├── tabs-image-02.jpg ├── tabs-image-03.jpg ├── testimonial-01.jpg ├── testimonial-02.jpg ├── testimonial-03.jpg └── video.mp4 ├── tailwind.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cruip Tutorials made with Next.js and Tailwind CSS 2 | 3 | Welcome to the repository of Cruip's guides & tutorials for **Next.js**. Here you will find the code for all the components we have developed and will develop, made in **TypeScript**, and ready to be integrated into your projects. 4 | 5 | If you're new to our articles, we recommend taking a look here: [Tailwind CSS Tutorials](https://cruip.com/tutorials/). If you're new to Cruip's ecosystem, learn more about our [Tailwind CSS templates](https://cruip.com/). 6 | 7 | ## Tutorial list 8 | 9 | - [How to Build a Modal Video Component with Tailwind CSS and Next.js](https://cruip.com/how-to-build-a-modal-video-component-with-tailwind-css-and-next-js/) 10 | - [How to Create a Pricing Table with a Monthly/Yearly Toggle in Tailwind CSS and Next.js](https://cruip.com/how-to-create-a-pricing-table-with-a-monthly-yearly-toggle-in-tailwind-css-and-next-js/) 11 | - [How to Create a Beautiful Particle Animation with HTML Canvas](https://cruip.com/how-to-create-a-beautiful-particle-animation-with-html-canvas/) 12 | - [How to Create a Spotlight Card Hover Effect with Tailwind CSS](https://cruip.com/how-to-create-a-spotlight-card-hover-effect-with-tailwind-css/) 13 | - [How to Build a Fancy Testimonial Slider with Tailwind CSS and Next.js](https://cruip.com/how-to-build-a-fancy-testimonial-slider-with-tailwind-css-and-next-js/) 14 | - [Using Tailwind CSS and Next.js to Create Animated and Accessible Tabs](https://cruip.com/using-tailwind-css-and-next-js-to-create-animated-and-accessible-tabs/) 15 | - [How to Add Custom Fonts in Next.js and Tailwind CSS Templates](https://cruip.com/how-to-add-custom-fonts-in-next-js-and-tailwind-css-templates/) 16 | - [How to Create a Feature Comparison Table with Tailwind CSS](https://cruip.com/how-to-create-a-feature-comparison-table-with-tailwind-css/) 17 | - [Building a Simple Animated Accordion Component with Tailwind CSS](https://cruip.com/building-a-simple-animated-accordion-component-with-tailwind-css/) 18 | - [Creating a Sliding Text Animation with Tailwind CSS](https://cruip.com/creating-a-sliding-text-animation-with-tailwind-css/) 19 | - [Create an Infinite Horizontal Scroll Animation with Tailwind CSS](https://cruip.com/create-an-infinite-horizontal-scroll-animation-with-tailwind-css/) 20 | - [How to Create a Sticky On Scroll Effect with JavaScript](https://cruip.com/how-to-create-a-sticky-on-scroll-effect-with-javascript/) 21 | - [Create a Carousel with Progress Indicators using Tailwind and Next.js](https://cruip.com/create-a-carousel-with-progress-indicators-using-tailwind-and-nextjs/) 22 | - [Styling the Active Link with Next.js and Tailwind CSS ](https://cruip.com/styling-the-active-link-with-nextjs-and-tailwind-css/) 23 | - [Building a Documentation Search Modal with Next.js](https://cruip.com/building-a-documentation-search-modal-with-nextjs/) 24 | - [Active Link Animation with Tailwind CSS and Framer Motion](https://cruip.com/active-link-animation-with-tailwind-css-and-framer-motion/) 25 | - [How to Create a True Masonry with Next.js](https://cruip.com/how-to-create-a-true-masonry-with-nextjs/) 26 | - [4 Examples of Dropdown Menus with Tailwind CSS and Radix UI](https://cruip.com/4-examples-of-dropdown-menus-with-tailwind-css-and-radix-ui/) 27 | - [Animate a React Switch Toggle with Framer Motion](https://cruip.com/animate-a-react-switch-toggle-with-framer-motion/) 28 | - [Toggle Password Visibility with Tailwind CSS and Next.js](https://cruip.com/toggle-password-visibility-with-tailwind-css-and-nextjs/) 29 | - [Build a Password Field with Strength Indicator with Next.js](https://cruip.com/build-a-password-field-with-strength-indicator-with-nextjs/) 30 | - [Building a Composable Multiselect React Component](https://cruip.com/building-a-composable-multiselect-react-component/) 31 | 32 | ## Getting Started 33 | 34 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 35 | 36 | First, run the development server: 37 | 38 | ```bash 39 | npm run dev 40 | # or 41 | yarn dev 42 | # or 43 | pnpm dev 44 | ``` 45 | 46 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 47 | 48 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 49 | 50 | [http://localhost:3000/api/hello](http://localhost:3000/api/hello) is an endpoint that uses [Route Handlers](https://beta.nextjs.org/docs/routing/route-handlers). This endpoint can be edited in `app/api/hello/route.ts`. 51 | 52 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 53 | 54 | ## Learn More 55 | 56 | To learn more about Next.js, take a look at the following resources: 57 | 58 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 59 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 60 | 61 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 62 | 63 | ## Deploy on Vercel 64 | 65 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 66 | 67 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 68 | 69 | ## Terms of use 70 | 71 | We want you to get the most out of these resources, so here's what you need to know: 72 | 73 | 1. **Usage Freedom**: You are free to use these tutorials and demos for any purpose. Whether it's for personal projects, educational endeavors, or even commercial purposes, go ahead and make the most of them! 74 | 75 | 2. **Respect Our Work**: While you're free to use the tutorials and demos as-is, you are not allowed to redistribute them or create derivative works with the aim of making money. Let's keep it fair and respect each other's efforts. 76 | 77 | 3. **Attribute and Share the Love**: We kindly ask that you provide attribution when you use our work. It's easy, just retain any copyright notices or credits you find in the code. It helps spread the word about our project and allows others to discover these valuable resources too. 78 | 79 | 4. **Be Creative**: We love to see what you can do with our work! Feel free to create remixes or derivative works for non-commercial purposes. Play around, experiment, and have fun, as long as you stick to the same license terms. 80 | 81 | That's it! We believe in empowering the community with knowledge and resources, and we hope these tutorials and demos help you on your journey to creating fantastic projects with Tailwind CSS. 82 | 83 | If you have any questions or just want to say hello, don't hesitate to reach out to us at Cruip.com. 84 | 85 | Happy coding! 🚀 86 | -------------------------------------------------------------------------------- /app/active-link/customers/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Active Link - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import NavigationMenu from "./../navigation-menu"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function InnerPage() { 10 | return ( 11 | <> 12 |
13 |
14 | 15 |
16 |
17 | 18 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/active-link/navigation-menu.tsx: -------------------------------------------------------------------------------- 1 | import NavLink from "@/components/nav-link"; 2 | 3 | export default function NavigationMenu() { 4 | return ( 5 |
6 |
7 | 11 | 17 | 23 | 24 | 25 |
26 | 63 |
64 | 68 | Sign up 69 | 70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /app/active-link/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Active Link - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import NavigationMenu from "./navigation-menu"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function ActiveLinkPage() { 10 | return ( 11 | <> 12 |
13 |
14 | 15 |
16 |
17 | 18 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/active-link/partners/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Active Link - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import NavigationMenu from "../navigation-menu"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function InnerPage() { 10 | return ( 11 | <> 12 |
13 |
14 | 15 |
16 |
17 | 18 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/active-link/team/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Active Link - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import NavigationMenu from "./../navigation-menu"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function InnerPage() { 10 | return ( 11 | <> 12 |
13 |
14 | 15 |
16 |
17 | 18 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/animated-accordion/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Animated Accordion - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import Accordion from "@/components/accordion"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function AnimatedAccordionPage() { 10 | const faqs = [ 11 | { 12 | title: "What are the advantages of your service?", 13 | text: "If you go over your organisations or user limit, a member of the team will reach out about bespoke pricing. In the meantime, our collaborative features won't appear in accounts or users that are over the 100-account or 1,000-user limit.", 14 | active: false, 15 | }, 16 | { 17 | title: 18 | "Are there any fees or commissions in addition to the monthly subscription?", 19 | text: "If you go over your organisations or user limit, a member of the team will reach out about bespoke pricing. In the meantime, our collaborative features won't appear in accounts or users that are over the 100-account or 1,000-user limit.", 20 | active: false, 21 | }, 22 | { 23 | title: "You really don't charge per user? Why not?", 24 | text: "If you go over your organisations or user limit, a member of the team will reach out about bespoke pricing. In the meantime, our collaborative features won't appear in accounts or users that are over the 100-account or 1,000-user limit.", 25 | active: false, 26 | }, 27 | { 28 | title: "What happens when I go over my monthly active limit?", 29 | text: "If you go over your organisations or user limit, a member of the team will reach out about bespoke pricing. In the meantime, our collaborative features won't appear in accounts or users that are over the 100-account or 1,000-user limit.", 30 | active: true, 31 | }, 32 | { 33 | title: "Can your service help me understand how to work with my product?", 34 | text: "If you go over your organisations or user limit, a member of the team will reach out about bespoke pricing. In the meantime, our collaborative features won't appear in accounts or users that are over the 100-account or 1,000-user limit.", 35 | active: false, 36 | }, 37 | { 38 | title: "Which third-party application do you integrate with?", 39 | text: "If you go over your organisations or user limit, a member of the team will reach out about bespoke pricing. In the meantime, our collaborative features won't appear in accounts or users that are over the 100-account or 1,000-user limit.", 40 | active: false, 41 | }, 42 | { 43 | title: "I have another question!", 44 | text: "If you go over your organisations or user limit, a member of the team will reach out about bespoke pricing. In the meantime, our collaborative features won't appear in accounts or users that are over the 100-account or 1,000-user limit.", 45 | active: false, 46 | }, 47 | ]; 48 | 49 | return ( 50 | <> 51 |
52 |
53 |

FAQs

54 | 55 |
56 | {faqs.map((faq, index) => ( 57 | 63 | {faq.text} 64 | 65 | ))} 66 |
67 |
68 |
69 | 70 | 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /app/animated-switch/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Animated Switch - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import Switch from "@/components/switch"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function AnimatedSwitchPage() { 10 | return ( 11 | <> 12 |
13 |
14 |
15 |
16 |
17 | 18 | 24 |
25 |
26 |
27 |
28 |
29 | 30 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/api/hello/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET(request: Request) { 2 | return new Response("Hello, Next.js!"); 3 | } 4 | -------------------------------------------------------------------------------- /app/api/og/route.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from "next/og"; 2 | 3 | export const runtime = "edge"; 4 | 5 | export async function GET(request: Request) { 6 | const interExtrabold = fetch( 7 | new URL("../../../public/Inter-ExtraBold.ttf", import.meta.url), 8 | ).then((res) => res.arrayBuffer()); 9 | 10 | try { 11 | const { searchParams } = new URL(request.url); 12 | 13 | const hasTitle = searchParams.has("title"); 14 | const title = hasTitle 15 | ? searchParams.get("title")?.slice(0, 100) 16 | : "Default title"; 17 | 18 | return new ImageResponse( 19 | ( 20 |
35 |
46 | {title} 47 |
48 | 53 |
54 | ), 55 | { 56 | width: 1200, 57 | height: 630, 58 | fonts: [ 59 | { 60 | name: "Inter", 61 | data: await interExtrabold, 62 | style: "normal", 63 | weight: 800, 64 | }, 65 | ], 66 | }, 67 | ); 68 | } catch (e: any) { 69 | console.log(`${e.message}`); 70 | return new Response(`Failed to generate the image`, { 71 | status: 500, 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/blur-reveal/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Sliding Text - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import BlurReveal from "@/components/blur-reveal"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function BlurRevealPage() { 10 | return ( 11 | <> 12 |
13 | 17 | 21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/dropdown-examples/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "4 Dropdown Examples - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import Banner from "@/components/banner"; 7 | import Dropdown01 from "@/components/dropdown-01"; 8 | import Dropdown02 from "@/components/dropdown-02"; 9 | import Dropdown03 from "@/components/dropdown-03"; 10 | import Dropdown04 from "@/components/dropdown-04"; 11 | 12 | export default function DropdownExamplesPage() { 13 | return ( 14 | <> 15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 | 34 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/fancy-testimonials-slider/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Fancy Testimonials Slider - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import TestimonialImg01 from "@/public/testimonial-01.jpg"; 7 | import TestimonialImg02 from "@/public/testimonial-02.jpg"; 8 | import TestimonialImg03 from "@/public/testimonial-03.jpg"; 9 | import FancyTestimonialsSlider from "@/components/fancy-testimonials-slider"; 10 | import Banner from "@/components/banner"; 11 | 12 | export default function FancyTestimonialSliderPage() { 13 | const testimonials = [ 14 | { 15 | img: TestimonialImg01, 16 | quote: 17 | "The ability to capture responses is a game-changer. If a user gets tired of the sign up and leaves, that data is still persisted. Additionally, it's great to select between formats.", 18 | name: "Jessie J", 19 | role: "Acme LTD", 20 | }, 21 | { 22 | img: TestimonialImg02, 23 | quote: 24 | "Having the power to capture user feedback is revolutionary. Even if a participant abandons the sign-up process midway, their valuable input remains intact.", 25 | name: "Nick V", 26 | role: "Malika Inc.", 27 | }, 28 | { 29 | img: TestimonialImg03, 30 | quote: 31 | "The functionality to capture responses is a true game-changer. Even if a user becomes fatigued during sign-up and abandons the process, their information remains stored.", 32 | name: "Amelia W", 33 | role: "Panda AI", 34 | }, 35 | ]; 36 | 37 | return ( 38 | <> 39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 | 47 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/app/favicon.ico -------------------------------------------------------------------------------- /app/feature-comparison-pricing-table/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Feature Comparison Pricing Table - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import FeatureComparisonPricing from "@/components/feature-comparison-pricing"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function FeatureComparisonPricingPage() { 10 | return ( 11 | <> 12 |
13 |
14 | 15 |
16 |
17 | 18 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @layer base { 5 | :root { 6 | --radius: 0.5rem; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | import "./globals.css"; 3 | 4 | const inter = Inter({ 5 | subsets: ["latin"], 6 | variable: "--font-inter", 7 | display: "swap", 8 | }); 9 | 10 | export const metadata = { 11 | title: "Create Next App", 12 | description: "Generated by create next app", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 23 | {children} 24 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/logo-carousel/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Logo Carousel - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import LogoCarousel from "@/components/logo-carousel"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function LogoCarouselPage() { 10 | return ( 11 | <> 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/masonry/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Masonry - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import TestimonialsGrid from "./testimonials-grid"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function MasonryPage() { 10 | return ( 11 | <> 12 |
13 |
14 | 15 |
16 |
17 | 18 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/masonry/testimonials-grid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import useMasonry from "@/components/utils/useMasonry"; 4 | import Testimonial from "@/components/testimonial"; 5 | import TestimonialImg01 from "@/public/masonry-01.jpg"; 6 | import TestimonialImg02 from "@/public/masonry-02.jpg"; 7 | import TestimonialImg03 from "@/public/masonry-03.jpg"; 8 | import TestimonialImg04 from "@/public/masonry-04.jpg"; 9 | import TestimonialImg05 from "@/public/masonry-05.jpg"; 10 | import TestimonialImg06 from "@/public/masonry-06.jpg"; 11 | import TestimonialImg07 from "@/public/masonry-07.jpg"; 12 | import TestimonialImg08 from "@/public/masonry-08.jpg"; 13 | import TestimonialImg09 from "@/public/masonry-09.jpg"; 14 | 15 | const testimonials = [ 16 | { 17 | img: TestimonialImg01, 18 | name: "Peter Lowe", 19 | username: "@peterlowex", 20 | date: "May 19, 2027", 21 | content: 22 | "As a founder, having a visually appealing and user-friendly website is essential. This tool not only helped me achieve that but also improved my site's performance and SEO. But the most important thing is that it is easy to use and understand.", 23 | channel: "Twitter", 24 | }, 25 | { 26 | img: TestimonialImg02, 27 | name: "Rodri Alba", 28 | username: "@rodri_spn", 29 | date: "Apr 12, 2027", 30 | content: 31 | "Simple has revolutionized the way I manage my work. Its intuitive interface and seamless functionality make staying organized effortless. I can't imagine my life without it.", 32 | channel: "Twitter", 33 | }, 34 | { 35 | img: TestimonialImg03, 36 | name: "Michele Lex", 37 | username: "@MikyBrown", 38 | date: "Mar 04, 2027", 39 | content: 40 | "I've tried several website builders before, but none were as user-friendly and versatile as this one. From design to functionality, it exceeded my expectations! It is easy to use and understand.", 41 | channel: "Twitter", 42 | }, 43 | { 44 | img: TestimonialImg04, 45 | name: "Michael Ross", 46 | username: "@michjack", 47 | date: "Jan 15, 2027", 48 | content: 49 | "Simple lives up to its name in every way. It's incredibly easy to use yet powerful.", 50 | channel: "Twitter", 51 | }, 52 | { 53 | img: TestimonialImg05, 54 | name: "Mike Bryan", 55 | username: "@mike0point7", 56 | date: "Dec 02, 2026", 57 | content: 58 | "I've been using Simple for over a year now, and it has transformed the way I work. It's intuitive, efficient.", 59 | channel: "Twitter", 60 | }, 61 | { 62 | img: TestimonialImg06, 63 | name: "Sarah Rodriguez", 64 | username: "@sararodriguez", 65 | date: "Nov 11, 2026", 66 | content: 67 | "Simple has made a significant impact on my business. It has helped me streamline my workflow, improve my productivity, and grow my brand. I highly recommend it to anyone looking to do the same. ", 68 | channel: "Twitter", 69 | }, 70 | { 71 | img: TestimonialImg07, 72 | name: "Duncan Mitch", 73 | username: "@lovingme_", 74 | date: "Oct 23, 2026", 75 | content: 76 | "I've been using Simple for a few months now, and I can't imagine my life without it. It has simplified my work process, increased my efficiency, and helped me achieve my goals faster.", 77 | channel: "Twitter", 78 | }, 79 | { 80 | img: TestimonialImg08, 81 | name: "Kavisha Mills", 82 | username: "@kavigirl99", 83 | date: "Sep 30, 2026", 84 | content: 85 | "Simple has been a game-changer for me. It has helped me stay organized, manage my tasks more efficiently, and improve my overall productivity. I can't recommend it enough! It has all the features I need to stay productive.", 86 | channel: "Twitter", 87 | }, 88 | { 89 | img: TestimonialImg09, 90 | name: "Dante Luzzi", 91 | username: "@dante1987", 92 | date: "Aug 17, 2026", 93 | content: 94 | "I've been using Simple for a while now, and it has transformed the way I work. It's intuitive, efficient.", 95 | channel: "Twitter", 96 | }, 97 | ]; 98 | 99 | export default function MasonryPage() { 100 | const masonryContainer = useMasonry(); 101 | 102 | return ( 103 |
107 | {testimonials.map((testimonial, index) => ( 108 | 109 | ))} 110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /app/modal-video/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Modal Video - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import VideoThumb from "@/public/modal-video-thumb.jpg"; 7 | import ModalVideo from "@/components/modal-video"; 8 | import Banner from "@/components/banner"; 9 | 10 | export default function ModalVideoPage() { 11 | return ( 12 | <> 13 |
14 |
15 |
16 | 25 |
26 |
27 |
28 | 29 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/multiselect/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Multiselect - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import MultipleSelector, { Option } from "@/components/multiselect"; 7 | import Banner from "@/components/banner"; 8 | 9 | const frameworks: Option[] = [ 10 | { 11 | value: "next.js", 12 | label: "Next.js", 13 | }, 14 | { 15 | value: "sveltekit", 16 | label: "SvelteKit", 17 | }, 18 | { 19 | value: "nuxt.js", 20 | label: "Nuxt.js", 21 | disable: true, 22 | }, 23 | { 24 | value: "remix", 25 | label: "Remix", 26 | }, 27 | { 28 | value: "astro", 29 | label: "Astro", 30 | }, 31 | { 32 | value: "angular", 33 | label: "Angular", 34 | }, 35 | { 36 | value: "vue", 37 | label: "Vue.js", 38 | }, 39 | { 40 | value: "react", 41 | label: "React", 42 | }, 43 | { 44 | value: "ember", 45 | label: "Ember.js", 46 | }, 47 | { 48 | value: "gatsby", 49 | label: "Gatsby", 50 | }, 51 | { 52 | value: "eleventy", 53 | label: "Eleventy", 54 | disable: true, 55 | }, 56 | { 57 | value: "solid", 58 | label: "SolidJS", 59 | }, 60 | { 61 | value: "preact", 62 | label: "Preact", 63 | }, 64 | { 65 | value: "qwik", 66 | label: "Qwik", 67 | }, 68 | { 69 | value: "alpine", 70 | label: "Alpine.js", 71 | }, 72 | { 73 | value: "lit", 74 | label: "Lit", 75 | }, 76 | ]; 77 | 78 | export default function MultiselectPage() { 79 | return ( 80 | <> 81 |
82 |
83 |
84 |
85 |
86 |
87 | No results found

} 97 | /> 98 |
99 |
100 |
101 |
102 |
103 |
104 | 105 | 109 | 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /app/particle-animation/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Particle Animation - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import Image from "next/image"; 7 | import Particles from "@/components/particles"; 8 | import Banner from "@/components/banner"; 9 | import Shape from "@/public/shape.svg"; 10 | 11 | export default function ModalVideoPage() { 12 | return ( 13 | <> 14 |
15 |
16 |
17 | {/* Illustration #1 */} 18 | 30 | 31 | {/* Illustration #2 */} 32 | 44 | 45 | {/* Particles animation */} 46 | 47 | 48 |
49 |

50 | Interactive Particle Animation 51 |

52 |
53 |

54 | Our landing page template works on all devices, so you only 55 | have to set it up once, and get beautiful results forever. 56 |

57 |
58 | 79 |
80 |
81 |
82 |
83 | 84 | 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /app/password-strength/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Password Strength Meter - Cruip Tutorials", 3 | description: "Page with password strength meter", 4 | }; 5 | 6 | import PasswordStrengthField from "@/components/password-strength-field"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function PasswordStrengthPage() { 10 | return ( 11 | <> 12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 | 26 | 27 | ); 28 | } -------------------------------------------------------------------------------- /app/pricing-table/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Pricing Table - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import PricingTable from "@/components/pricing-table"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function PricingTabsPage() { 10 | return ( 11 | <> 12 |
13 |
14 | 15 |
16 |
17 | 18 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/progress-slider/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Slider with Progress Indicator - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import SilderImg01 from "@/public/ps-image-01.png"; 7 | import SilderImg02 from "@/public/ps-image-02.png"; 8 | import SilderImg03 from "@/public/ps-image-03.png"; 9 | import SilderImg04 from "@/public/ps-image-04.png"; 10 | import SilderIcon01 from "@/public/ps-icon-01.svg"; 11 | import SilderIcon02 from "@/public/ps-icon-02.svg"; 12 | import SilderIcon03 from "@/public/ps-icon-03.svg"; 13 | import SilderIcon04 from "@/public/ps-icon-04.svg"; 14 | import ProgressSlider from "@/components/progress-slider"; 15 | import Banner from "@/components/banner"; 16 | 17 | export default function ProgressSliderPage() { 18 | const items = [ 19 | { 20 | img: SilderImg01, 21 | desc: "Omnichannel", 22 | buttonIcon: SilderIcon01, 23 | }, 24 | { 25 | img: SilderImg02, 26 | desc: "Multilingual", 27 | buttonIcon: SilderIcon02, 28 | }, 29 | { 30 | img: SilderImg03, 31 | desc: "Interpolate", 32 | buttonIcon: SilderIcon03, 33 | }, 34 | { 35 | img: SilderImg04, 36 | desc: "Enriched", 37 | buttonIcon: SilderIcon04, 38 | }, 39 | ]; 40 | 41 | return ( 42 | <> 43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 | 51 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /app/rotating-words/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Sliding Text - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import RotatingWords from "@/components/rotating-words"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function RotatingWordsPage() { 10 | return ( 11 | <> 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/search-modal/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Search Modal - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import SearchModal from "@/components/search-modal"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function SearchModalPage() { 10 | return ( 11 | <> 12 |
13 |
14 | 15 |
16 |
17 | 18 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/show-password/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Show Password Toggle - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import PasswordField from "@/components/password-field"; 7 | import Banner from "@/components/banner"; 8 | 9 | export default function AnimatedSwitchPage() { 10 | return ( 11 | <> 12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/sliding-active-link/nav-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | createContext, 5 | Dispatch, 6 | SetStateAction, 7 | useContext, 8 | useState, 9 | } from "react"; 10 | 11 | type ContextProps = { 12 | activeLink: string; 13 | setActiveLink: Dispatch>; 14 | }; 15 | 16 | const NavContext = createContext({ 17 | activeLink: "", 18 | setActiveLink: (): string => "", 19 | }); 20 | 21 | export default function NavProvider({ 22 | children, 23 | }: { 24 | children: React.ReactNode; 25 | }) { 26 | const [activeLink, setActiveLink] = useState(""); 27 | 28 | return ( 29 | 30 | {children} 31 | 32 | ); 33 | } 34 | 35 | export const useNavProvider = () => useContext(NavContext); 36 | -------------------------------------------------------------------------------- /app/sliding-active-link/navigation-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRef, useState, useEffect } from "react"; 4 | import { useNavProvider } from "./nav-provider"; 5 | import Link from "next/link"; 6 | import { motion } from "framer-motion"; 7 | 8 | export default function NavigationMenu({ 9 | links, 10 | }: { 11 | links: { title: string; slug: string }[]; 12 | }) { 13 | const { activeLink, setActiveLink } = useNavProvider(); 14 | const activeLinkRef = useRef(null); 15 | const [animationProps, setAnimationProps] = useState({ 16 | left: 0, 17 | width: 0, 18 | }); 19 | 20 | // check if the url has a hash and if so, set the active link 21 | useEffect(() => { 22 | const url = window.location.hash; 23 | if (url) { 24 | const link = url.replace("#", ""); 25 | setActiveLink(link); 26 | } 27 | }, []); 28 | 29 | // update the position and width of the active link underline 30 | useEffect(() => { 31 | const updateActiveLink = () => { 32 | if (activeLinkRef.current) { 33 | const { width } = activeLinkRef.current.getBoundingClientRect(); 34 | const left = activeLinkRef.current.offsetLeft; 35 | setAnimationProps({ 36 | left, 37 | width, 38 | }); 39 | } 40 | }; 41 | 42 | updateActiveLink(); 43 | window.addEventListener("resize", updateActiveLink); 44 | 45 | return () => { 46 | window.removeEventListener("resize", updateActiveLink); 47 | }; 48 | }, [activeLink]); 49 | 50 | return ( 51 |
52 |
53 |
54 | 74 | 97 | 105 |
106 |
107 |
108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /app/sliding-active-link/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Sliding Active Link - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import NavProvider from "./nav-provider"; 7 | import NavigationMenu from "./navigation-menu"; 8 | import Section from "./section"; 9 | import Banner from "@/components/banner"; 10 | 11 | const sections = [ 12 | { 13 | title: "Home", 14 | slug: "home", 15 | }, 16 | { 17 | title: "Customers", 18 | slug: "customers", 19 | }, 20 | { 21 | title: "Partners", 22 | slug: "partners", 23 | }, 24 | { 25 | title: "Team", 26 | slug: "team", 27 | }, 28 | ]; 29 | 30 | export default function SlidingActiveLinkPage() { 31 | return ( 32 | <> 33 |
34 | 35 | 36 |
37 | {sections.map((section) => ( 38 |
39 | ))} 40 |
41 |
42 |
43 | 44 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /app/sliding-active-link/section.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRef, useEffect } from "react"; 4 | import { useNavProvider } from "./nav-provider"; 5 | import { useInView } from "framer-motion"; 6 | 7 | export default function PageSection({ 8 | section, 9 | }: { 10 | section: { title: string; slug: string }; 11 | }) { 12 | const ref = useRef(null); 13 | const { setActiveLink } = useNavProvider(); 14 | 15 | const isInView = useInView(ref, { 16 | margin: "-50% 0px -50% 0px", 17 | }); 18 | 19 | useEffect(() => { 20 | if (isInView) { 21 | setActiveLink(section.slug); 22 | } 23 | }, [isInView]); 24 | 25 | return ( 26 |
31 |

{section.title}

32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/social-preview/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Social Metadata - Cruip Tutorials", 3 | description: 4 | "A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.", 5 | openGraph: { 6 | title: "Generate Dynamic Open Graph and Twitter Images in Next.js", 7 | description: 8 | "A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.", 9 | type: "article", 10 | url: "https://cruip-tutorials-next.vercel.app/social-preview", 11 | images: [ 12 | { 13 | url: "https://cruip-tutorials-next.vercel.app/api/og?title=Generate Dynamic Open Graph and Twitter Images in Next.js", 14 | }, 15 | ], 16 | }, 17 | twitter: { 18 | card: "summary_large_image", 19 | title: "Generate Dynamic Open Graph and Twitter Images in Next.js", 20 | description: 21 | "A guide on how to optimize SEO with static and dynamic metatags using Next.js 13's new Metadata API.", 22 | images: [ 23 | "https://cruip-tutorials-next.vercel.app/api/og?title=Generate Dynamic Open Graph and Twitter Images in Next.js", 24 | ], 25 | }, 26 | }; 27 | 28 | import Banner from "@/components/banner"; 29 | 30 | export default function SocialPreviewPage() { 31 | return ( 32 | <> 33 |
34 |
35 |
36 |
37 | Generate Dynamic Open Graph and Twitter Images in Next.js 38 |
39 |

40 | Share this page on Facebook and Twitter to see the preview image 41 |

42 |
43 |
44 |
45 | 46 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/sticky-scrolling/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Sticky Scrolling Effect - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import Image from "next/image"; 7 | import Illustration01 from "@/public/illustration-01.png"; 8 | import Illustration02 from "@/public/illustration-02.png"; 9 | import Illustration03 from "@/public/illustration-03.png"; 10 | import Illustration04 from "@/public/illustration-04.png"; 11 | import StickyScrolling from "@/components/sticky-scrolling"; 12 | import Banner from "@/components/banner"; 13 | 14 | export default function StickyScrollingPage() { 15 | const sections = [ 16 | { 17 | img: Illustration01, 18 | label: "Integrated Knowledge", 19 | title: "Support your users with popular topics", 20 | text: "Statistics show that people browsing your webpage who receive live assistance with a chat widget are more likely to make a purchase.", 21 | }, 22 | { 23 | img: Illustration02, 24 | label: "Authentic Experiences", 25 | title: "Personalise the support experience", 26 | text: "Statistics show that people browsing your webpage who receive live assistance with a chat widget are more likely to make a purchase.", 27 | }, 28 | { 29 | img: Illustration03, 30 | label: "Live Assistance", 31 | title: "Scale your sales using automation", 32 | text: "Statistics show that people browsing your webpage who receive live assistance with a chat widget are more likely to make a purchase.", 33 | }, 34 | { 35 | img: Illustration04, 36 | label: "Satisfaction Guaranteed", 37 | title: "Make customer satisfaction easier", 38 | text: "Statistics show that people browsing your webpage who receive live assistance with a chat widget are more likely to make a purchase.", 39 | }, 40 | ]; 41 | 42 | return ( 43 | <> 44 |
45 |
46 |
47 | Scroll down 48 |
49 | 50 | 51 |
52 | {sections.map((section, index) => ( 53 |
60 |
61 |
62 |
63 |
64 | {section.label} 65 | 71 | 72 | 73 |
74 |

75 | {section.title} 76 |

77 |

{section.text}

78 |
79 |
80 |
81 | {section.title} 87 |
88 |
89 |
90 | ))} 91 |
92 |
93 | 94 |
95 | Scroll up 96 |
97 |
98 |
99 | 100 | 104 | 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /app/unconventional-tabs/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Unconventional Tabs - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import TabImage01 from "@/public/tabs-image-01.jpg"; 7 | import Tab0Image2 from "@/public/tabs-image-02.jpg"; 8 | import Tab0Image3 from "@/public/tabs-image-03.jpg"; 9 | import UnconventionalTabs from "@/components/unconventional-tabs"; 10 | import Banner from "@/components/banner"; 11 | 12 | const tabs = [ 13 | { 14 | title: "Lassen Peak", 15 | img: TabImage01, 16 | tag: "Mountain", 17 | excerpt: 18 | "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.", 19 | link: "#0", 20 | }, 21 | { 22 | title: "Mount Shasta", 23 | img: Tab0Image2, 24 | tag: "Mountain", 25 | excerpt: 26 | "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.", 27 | link: "#0", 28 | }, 29 | { 30 | title: "Eureka Peak", 31 | img: Tab0Image3, 32 | tag: "Mountain", 33 | excerpt: 34 | "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.", 35 | link: "#0", 36 | }, 37 | ]; 38 | 39 | export default function UnconventionalTabsPage() { 40 | return ( 41 | <> 42 |
43 |
44 | 45 |
46 |
47 | 48 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /app/vertical-timelines/page.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Vertical Timelines - Cruip Tutorials", 3 | description: "Page description", 4 | }; 5 | 6 | import VerticalTimeline01 from "@/components/vertical-timeline-01"; 7 | import VerticalTimeline02 from "@/components/vertical-timeline-02"; 8 | import VerticalTimeline03 from "@/components/vertical-timeline-03"; 9 | import Banner from "@/components/banner"; 10 | 11 | export default function VerticalTimelinePage() { 12 | const timelineItems01 = [ 13 | { 14 | date: "May, 2020", 15 | label: "The origin", 16 | title: "Acme was founded in Milan, Italy", 17 | content: 18 | "Pretium lectus quam id leo. Urna et pharetra pharetra massa massa. Adipiscing enim eu neque aliquam vestibulum morbi blandit cursus risus.", 19 | }, 20 | { 21 | date: "May, 2021", 22 | label: "The milestone", 23 | title: "Reached 5K customers", 24 | content: 25 | "Pretium lectus quam id leo. Urna et pharetra pharetra massa massa. Adipiscing enim eu neque aliquam vestibulum morbi blandit cursus risus.", 26 | }, 27 | { 28 | date: "May, 2022", 29 | label: "The acquisitions", 30 | title: "Acquired various companies, inluding Technology Inc.", 31 | content: 32 | "Pretium lectus quam id leo. Urna et pharetra pharetra massa massa. Adipiscing enim eu neque aliquam vestibulum morbi blandit cursus risus.", 33 | }, 34 | { 35 | date: "May, 2023", 36 | label: "The IPO", 37 | title: "Acme went public at the New York Stock Exchange", 38 | content: 39 | "Pretium lectus quam id leo. Urna et pharetra pharetra massa massa. Adipiscing enim eu neque aliquam vestibulum morbi blandit cursus risus.", 40 | }, 41 | ]; 42 | 43 | const timelineItems02 = [ 44 | { 45 | completed: true, 46 | date: "08/06/2023", 47 | title: "Order Placed", 48 | content: 49 | "Pretium lectus quam id leo. Urna et pharetra aliquam vestibulum morbi blandit cursus risus.", 50 | }, 51 | { 52 | completed: true, 53 | date: "09/06/2023", 54 | title: "Order Shipped", 55 | content: 56 | "Pretium lectus quam id leo. Urna et pharetra aliquam vestibulum morbi blandit cursus risus.", 57 | }, 58 | { 59 | completed: true, 60 | date: "10/06/2023", 61 | title: "In Transit", 62 | content: 63 | "Pretium lectus quam id leo. Urna et pharetra aliquam vestibulum morbi blandit cursus risus.", 64 | }, 65 | { 66 | completed: true, 67 | date: "12/06/2023", 68 | title: "Out of Delivery", 69 | content: 70 | "Pretium lectus quam id leo. Urna et pharetra aliquam vestibulum morbi blandit cursus risus.", 71 | }, 72 | { 73 | completed: false, 74 | deliver: true, 75 | date: "12/08/2023", 76 | title: "Delivered", 77 | content: 78 | "Pretium lectus quam id leo. Urna et pharetra aliquam vestibulum morbi blandit cursus risus.", 79 | }, 80 | ]; 81 | 82 | const timelineItems03 = [ 83 | { 84 | date: "Apr 7, 2024", 85 | author: "Mark Mikrol", 86 | type: "open", 87 | content: 88 | "Various versions have evolved over the years, sometimes by accident, sometimes on purpose injected humour and the like.", 89 | }, 90 | { 91 | date: "Apr 7, 2024", 92 | author: "John Mirkovic", 93 | type: "comment", 94 | content: 95 | "If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text.", 96 | }, 97 | { 98 | date: "Apr 8, 2024", 99 | author: "Vlad Patterson", 100 | type: "comment", 101 | content: 102 | "Letraset sheets containing passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Ipsum.", 103 | }, 104 | { 105 | date: "Apr 8, 2024", 106 | author: "Mila Capentino", 107 | type: "comment", 108 | content: 109 | "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.", 110 | }, 111 | { 112 | date: "Apr 9, 2024", 113 | author: "Mark Mikrol", 114 | type: "close", 115 | content: "If you are going to use a passage of Lorem Ipsum!", 116 | }, 117 | ]; 118 | 119 | return ( 120 | <> 121 |
122 |
123 |
124 |
125 | 126 |
127 | 128 |
129 | 130 |
131 | 132 |
133 | 134 |
135 |
136 |
137 |
138 | 139 | 143 | 144 | ); 145 | } 146 | -------------------------------------------------------------------------------- /components/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | type AccordionpProps = { 6 | children: React.ReactNode; 7 | title: string; 8 | id: string; 9 | active?: boolean; 10 | }; 11 | 12 | export default function Accordion({ 13 | children, 14 | title, 15 | id, 16 | active = false, 17 | }: AccordionpProps) { 18 | const [accordionOpen, setAccordionOpen] = useState(false); 19 | 20 | useEffect(() => { 21 | setAccordionOpen(active); 22 | }, []); 23 | 24 | return ( 25 |
26 |

27 | 59 |

60 |
66 |
67 |

{children}

68 |
69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /components/banner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | interface BannerProps { 6 | tutorialUrl: string; 7 | downloadUrl: string; 8 | } 9 | 10 | export default function Banner({ tutorialUrl, downloadUrl }: BannerProps) { 11 | const [bannerOpen, setBannerOpen] = useState(true); 12 | 13 | return ( 14 | <> 15 | {bannerOpen && ( 16 |
17 |
18 | 43 | 55 |
56 |
57 | )} 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /components/blur-reveal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { motion } from "framer-motion"; 5 | 6 | const transition = { duration: 1, ease: [0.25, 0.1, 0.25, 1] }; 7 | const variants = { 8 | hidden: { filter: "blur(10px)", transform: "translateY(20%)", opacity: 0 }, 9 | visible: { filter: "blur(0)", transform: "translateY(0)", opacity: 1 }, 10 | }; 11 | 12 | const text = "The website builder you're looking for is here"; 13 | 14 | export default function BlurReveal() { 15 | const words = text.split(" "); 16 | 17 | return ( 18 | 23 |

24 | {words.map((word, index) => ( 25 | 26 | 31 | {word} 32 | 33 | {index < words.length - 1 && " "} 34 | 35 | ))} 36 |

37 | 42 | Simple is a modern website builder powered by AI that is changing how 43 | companies create user interfaces together. 44 | 45 |
46 | 47 | 51 | Start Free Trial 52 | 53 | 54 | 55 | 59 | Learn More 60 | 61 | 62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /components/command.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { type DialogProps } from "@radix-ui/react-dialog"; 4 | import { Command as CommandPrimitive } from "cmdk"; 5 | import { Search } from "lucide-react"; 6 | import * as React from "react"; 7 | 8 | import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/dialog"; 9 | import { cn } from "@/lib/utils"; 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )); 24 | Command.displayName = CommandPrimitive.displayName; 25 | 26 | const CommandDialog = ({ children, ...props }: DialogProps) => { 27 | return ( 28 | 29 | {/* Temporary fix to silence console warning */} 30 | {/* Temporary fix to silence console warning */} 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | const CommandInput = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 |
45 | 46 | 54 |
55 | )); 56 | 57 | CommandInput.displayName = CommandPrimitive.Input.displayName; 58 | 59 | const CommandList = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, ...props }, ref) => ( 63 | 68 | )); 69 | 70 | CommandList.displayName = CommandPrimitive.List.displayName; 71 | 72 | const CommandEmpty = React.forwardRef< 73 | React.ElementRef, 74 | React.ComponentPropsWithoutRef 75 | >((props, ref) => ( 76 | 77 | )); 78 | 79 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName; 80 | 81 | const CommandGroup = React.forwardRef< 82 | React.ElementRef, 83 | React.ComponentPropsWithoutRef 84 | >(({ className, ...props }, ref) => ( 85 | 93 | )); 94 | 95 | CommandGroup.displayName = CommandPrimitive.Group.displayName; 96 | 97 | const CommandSeparator = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 106 | )); 107 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName; 108 | 109 | const CommandItem = React.forwardRef< 110 | React.ElementRef, 111 | React.ComponentPropsWithoutRef 112 | >(({ className, ...props }, ref) => ( 113 | 121 | )); 122 | 123 | CommandItem.displayName = CommandPrimitive.Item.displayName; 124 | 125 | const CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => { 126 | return ( 127 | 134 | ); 135 | }; 136 | CommandShortcut.displayName = "CommandShortcut"; 137 | 138 | export { 139 | Command, 140 | CommandDialog, 141 | CommandEmpty, 142 | CommandGroup, 143 | CommandInput, 144 | CommandItem, 145 | CommandList, 146 | CommandSeparator, 147 | CommandShortcut, 148 | }; 149 | -------------------------------------------------------------------------------- /components/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 4 | import { X } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Dialog = DialogPrimitive.Root; 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger; 12 | 13 | const DialogPortal = DialogPrimitive.Portal; 14 | 15 | const DialogClose = DialogPrimitive.Close; 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )); 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 53 | Close 54 | 55 | 56 | 57 | )); 58 | DialogContent.displayName = DialogPrimitive.Content.displayName; 59 | 60 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( 61 |
62 | ); 63 | DialogHeader.displayName = "DialogHeader"; 64 | 65 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( 66 |
70 | ); 71 | DialogFooter.displayName = "DialogFooter"; 72 | 73 | const DialogTitle = React.forwardRef< 74 | React.ElementRef, 75 | React.ComponentPropsWithoutRef 76 | >(({ className, ...props }, ref) => ( 77 | 82 | )); 83 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 84 | 85 | const DialogDescription = React.forwardRef< 86 | React.ElementRef, 87 | React.ComponentPropsWithoutRef 88 | >(({ className, ...props }, ref) => ( 89 | 94 | )); 95 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 96 | 97 | export { 98 | Dialog, 99 | DialogClose, 100 | DialogContent, 101 | DialogDescription, 102 | DialogFooter, 103 | DialogHeader, 104 | DialogOverlay, 105 | DialogPortal, 106 | DialogTitle, 107 | DialogTrigger, 108 | }; 109 | -------------------------------------------------------------------------------- /components/dropdown-01.tsx: -------------------------------------------------------------------------------- 1 | import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 2 | 3 | export default function Dropdown01() { 4 | const itemClassName = 5 | "relative flex cursor-default select-none items-center rounded-sm px-4 py-2 text-sm outline-none transition-colors focus:bg-slate-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50"; 6 | 7 | return ( 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | Go Pro 28 | 29 | 40 | 41 | 42 | 43 | Manage account 44 | 45 | 46 | Feedback 47 | 48 | 49 | Log out 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /components/dropdown-02.tsx: -------------------------------------------------------------------------------- 1 | import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 2 | 3 | export default function Dropdown02() { 4 | const itemClassName = 5 | "relative flex cursor-default select-none items-center rounded-sm px-4 py-2 text-sm outline-none transition-colors focus:bg-slate-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50"; 6 | const separatorClassName = "-mx-1 my-1 h-px bg-slate-200"; 7 | 8 | return ( 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | Go Pro 30 | 31 | 42 | 43 | 44 | 45 | Invite friends 46 | 47 | 48 | 49 | 50 | 51 | Feedback 52 | 53 | 54 | Manage account 55 | 56 | 57 | 58 | 59 | 60 | Preferences 61 | 62 | 63 | Help center 64 | 65 | 66 | Log out 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /components/dropdown-03.tsx: -------------------------------------------------------------------------------- 1 | import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 2 | 3 | export default function Dropdown03() { 4 | const itemClassName = 5 | "relative flex cursor-default select-none items-center rounded-sm px-4 py-2 text-sm outline-none transition-colors focus:bg-slate-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50"; 6 | const separatorClassName = "-mx-1 my-1 h-px bg-slate-200"; 7 | 8 | return ( 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 36 | 37 | 38 | Go Pro 39 | 40 | 41 | 52 | 53 | 54 | 55 | 56 | 62 | 63 | 64 | Invite friends 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 78 | 79 | 80 | Feedback 81 | 82 | 83 | 84 | 85 | 91 | 92 | 93 | Manage account 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 107 | 111 | 112 | Preferences 113 | 114 | 115 | 116 | 117 | 123 | 124 | 125 | Help center 126 | 127 | 128 | 129 | 130 | 136 | 137 | 138 | 139 | Log out 140 | 141 | 142 | 143 | 144 | 145 | 146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /components/dropdown-04.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 3 | import UserImage from "@/public/dropdown-user.jpg"; 4 | 5 | export default function Dropdown04() { 6 | const itemClassName = 7 | "relative flex cursor-default select-none items-center rounded-sm px-4 py-2 text-sm outline-none transition-colors focus:bg-slate-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50"; 8 | const separatorClassName = "-mx-1 my-1 h-px bg-slate-200"; 9 | 10 | return ( 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | User 38 | 39 |

40 | Chris Bostian 41 |

42 | 43 | @chrisbostian 44 | 45 |
46 |
47 |
48 | 49 | 50 | 51 | Go Pro 52 | 53 | 64 | 65 | 66 | 67 | Invite friends 68 | 69 | 70 | 71 | 72 | 73 | Feedback 74 | 75 | 76 | Manage account 77 | 78 | 79 | 80 | 81 | 82 | Preferences 83 | 84 | 85 | Help center 86 | 87 | 88 | Log out 89 | 90 | 91 |
92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /components/fancy-testimonials-slider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useRef, useEffect } from "react"; 4 | import Image, { StaticImageData } from "next/image"; 5 | import { Transition } from "@headlessui/react"; 6 | 7 | interface Testimonial { 8 | img: StaticImageData; 9 | quote: string; 10 | name: string; 11 | role: string; 12 | } 13 | 14 | export default function FancyTestimonialsSlider({ 15 | testimonials, 16 | }: { 17 | testimonials: Testimonial[]; 18 | }) { 19 | const testimonialsRef = useRef(null); 20 | const [active, setActive] = useState(0); 21 | const [autorotate, setAutorotate] = useState(true); 22 | const autorotateTiming: number = 7000; 23 | 24 | useEffect(() => { 25 | if (!autorotate) return; 26 | const interval = setInterval(() => { 27 | setActive( 28 | active + 1 === testimonials.length ? 0 : (active) => active + 1, 29 | ); 30 | }, autorotateTiming); 31 | return () => clearInterval(interval); 32 | }, [active, autorotate]); 33 | 34 | const heightFix = () => { 35 | if (testimonialsRef.current && testimonialsRef.current.parentElement) 36 | testimonialsRef.current.parentElement.style.height = `${testimonialsRef.current.clientHeight}px`; 37 | }; 38 | 39 | useEffect(() => { 40 | heightFix(); 41 | }, []); 42 | 43 | return ( 44 |
45 | {/* Testimonial image */} 46 |
47 |
48 |
49 | {testimonials.map((testimonial, index) => ( 50 | heightFix()} 62 | > 63 | {testimonial.name} 70 | 71 | ))} 72 |
73 |
74 |
75 | {/* Text */} 76 |
77 |
78 | {testimonials.map((testimonial, index) => ( 79 | heightFix()} 89 | > 90 |
91 | {testimonial.quote} 92 |
93 |
94 | ))} 95 |
96 |
97 | {/* Buttons */} 98 |
99 | {testimonials.map((testimonial, index) => ( 100 | 116 | ))} 117 |
118 |
119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /components/logo-carousel.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Facebook from "@/public/facebook.svg"; 3 | import Disney from "@/public/disney.svg"; 4 | import Airbnb from "@/public/airbnb.svg"; 5 | import Apple from "@/public/apple.svg"; 6 | import Spark from "@/public/spark.svg"; 7 | import Samsung from "@/public/samsung.svg"; 8 | import Quora from "@/public/quora.svg"; 9 | import Sass from "@/public/sass.svg"; 10 | 11 | export default function LogoCarousel() { 12 | const logos = [ 13 | { src: Facebook, alt: "Facebook" }, 14 | { src: Disney, alt: "Disney" }, 15 | { src: Airbnb, alt: "Airbnb" }, 16 | { src: Apple, alt: "Apple" }, 17 | { src: Spark, alt: "Spark" }, 18 | { src: Samsung, alt: "Samsung" }, 19 | { src: Quora, alt: "Quora" }, 20 | { src: Sass, alt: "Sass" }, 21 | ]; 22 | 23 | return ( 24 |
25 |
    26 | {logos.map((logo, index) => ( 27 |
  • 28 | {logo.alt} 29 |
  • 30 | ))} 31 |
32 | 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/modal-video.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useRef, Fragment } from "react"; 4 | import type { StaticImageData } from "next/image"; 5 | import { 6 | Dialog, 7 | DialogPanel, 8 | Transition, 9 | TransitionChild, 10 | } from "@headlessui/react"; 11 | import Image from "next/image"; 12 | 13 | interface ModalVideoProps { 14 | thumb: StaticImageData; 15 | thumbWidth: number; 16 | thumbHeight: number; 17 | thumbAlt: string; 18 | video: string; 19 | videoWidth: number; 20 | videoHeight: number; 21 | } 22 | 23 | export default function ModalVideo({ 24 | thumb, 25 | thumbWidth, 26 | thumbHeight, 27 | thumbAlt, 28 | video, 29 | videoWidth, 30 | videoHeight, 31 | }: ModalVideoProps) { 32 | const [modalOpen, setModalOpen] = useState(false); 33 | const videoRef = useRef(null); 34 | 35 | return ( 36 |
37 | {/* Video thumbnail */} 38 | 73 | {/* End: Video thumbnail */} 74 | 75 | videoRef.current?.play()} 79 | > 80 | setModalOpen(false)}> 81 | {/* Modal backdrop */} 82 | 123 | 124 |
125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /components/nav-link.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname } from "next/navigation"; 4 | import Link from "next/link"; 5 | 6 | export default function NavLink({ 7 | href, 8 | exact = false, 9 | children, 10 | className, 11 | ...props 12 | }: { 13 | href: string; 14 | exact?: boolean; 15 | className?: string; 16 | children: React.ReactNode; 17 | }) { 18 | const pathname = usePathname(); 19 | const isActive = exact ? pathname === href : pathname.startsWith(href); 20 | const newClassName = isActive ? `${className} active` : className; 21 | 22 | return ( 23 | 24 | {children} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /components/particles.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useRef, useEffect } from "react"; 4 | import useMousePosition from "./utils/useMousePosition"; 5 | 6 | interface ParticlesProps { 7 | className?: string; 8 | quantity?: number; 9 | staticity?: number; 10 | ease?: number; 11 | } 12 | 13 | export default function Particles({ 14 | className = "", 15 | quantity = 30, 16 | staticity = 50, 17 | ease = 50, 18 | }: ParticlesProps) { 19 | const canvasRef = useRef(null); 20 | const canvasContainerRef = useRef(null); 21 | const context = useRef(null); 22 | const circles = useRef([]); 23 | const mousePosition = useMousePosition(); 24 | const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); 25 | const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 }); 26 | const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1; 27 | 28 | useEffect(() => { 29 | if (canvasRef.current) { 30 | context.current = canvasRef.current.getContext("2d"); 31 | } 32 | initCanvas(); 33 | animate(); 34 | window.addEventListener("resize", initCanvas); 35 | 36 | return () => { 37 | window.removeEventListener("resize", initCanvas); 38 | }; 39 | }, []); 40 | 41 | useEffect(() => { 42 | onMouseMove(); 43 | }, [mousePosition.x, mousePosition.y]); 44 | 45 | const initCanvas = () => { 46 | resizeCanvas(); 47 | drawParticles(); 48 | }; 49 | 50 | const onMouseMove = () => { 51 | if (canvasRef.current) { 52 | const rect = canvasRef.current.getBoundingClientRect(); 53 | const { w, h } = canvasSize.current; 54 | const x = mousePosition.x - rect.left - w / 2; 55 | const y = mousePosition.y - rect.top - h / 2; 56 | const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2; 57 | if (inside) { 58 | mouse.current.x = x; 59 | mouse.current.y = y; 60 | } 61 | } 62 | }; 63 | 64 | const resizeCanvas = () => { 65 | if (canvasContainerRef.current && canvasRef.current && context.current) { 66 | circles.current.length = 0; 67 | canvasSize.current.w = canvasContainerRef.current.offsetWidth; 68 | canvasSize.current.h = canvasContainerRef.current.offsetHeight; 69 | canvasRef.current.width = canvasSize.current.w * dpr; 70 | canvasRef.current.height = canvasSize.current.h * dpr; 71 | canvasRef.current.style.width = canvasSize.current.w + "px"; 72 | canvasRef.current.style.height = canvasSize.current.h + "px"; 73 | context.current.scale(dpr, dpr); 74 | } 75 | }; 76 | 77 | type Circle = { 78 | x: number; 79 | y: number; 80 | translateX: number; 81 | translateY: number; 82 | size: number; 83 | alpha: number; 84 | targetAlpha: number; 85 | dx: number; 86 | dy: number; 87 | magnetism: number; 88 | }; 89 | 90 | const circleParams = (): Circle => { 91 | const x = Math.floor(Math.random() * canvasSize.current.w); 92 | const y = Math.floor(Math.random() * canvasSize.current.h); 93 | const translateX = 0; 94 | const translateY = 0; 95 | const size = Math.floor(Math.random() * 2) + 1; 96 | const alpha = 0; 97 | const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1)); 98 | const dx = (Math.random() - 0.5) * 0.2; 99 | const dy = (Math.random() - 0.5) * 0.2; 100 | const magnetism = 0.1 + Math.random() * 4; 101 | return { 102 | x, 103 | y, 104 | translateX, 105 | translateY, 106 | size, 107 | alpha, 108 | targetAlpha, 109 | dx, 110 | dy, 111 | magnetism, 112 | }; 113 | }; 114 | 115 | const drawCircle = (circle: Circle, update = false) => { 116 | if (context.current) { 117 | const { x, y, translateX, translateY, size, alpha } = circle; 118 | context.current.translate(translateX, translateY); 119 | context.current.beginPath(); 120 | context.current.arc(x, y, size, 0, 2 * Math.PI); 121 | context.current.fillStyle = `rgba(255, 255, 255, ${alpha})`; 122 | context.current.fill(); 123 | context.current.setTransform(dpr, 0, 0, dpr, 0, 0); 124 | 125 | if (!update) { 126 | circles.current.push(circle); 127 | } 128 | } 129 | }; 130 | 131 | const clearContext = () => { 132 | if (context.current) { 133 | context.current.clearRect( 134 | 0, 135 | 0, 136 | canvasSize.current.w, 137 | canvasSize.current.h, 138 | ); 139 | } 140 | }; 141 | 142 | const drawParticles = () => { 143 | clearContext(); 144 | const particleCount = quantity; 145 | for (let i = 0; i < particleCount; i++) { 146 | const circle = circleParams(); 147 | drawCircle(circle); 148 | } 149 | }; 150 | 151 | const remapValue = ( 152 | value: number, 153 | start1: number, 154 | end1: number, 155 | start2: number, 156 | end2: number, 157 | ): number => { 158 | const remapped = 159 | ((value - start1) * (end2 - start2)) / (end1 - start1) + start2; 160 | return remapped > 0 ? remapped : 0; 161 | }; 162 | 163 | const animate = () => { 164 | clearContext(); 165 | circles.current.forEach((circle: Circle, i: number) => { 166 | // Handle the alpha value 167 | const edge = [ 168 | circle.x + circle.translateX - circle.size, // distance from left edge 169 | canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge 170 | circle.y + circle.translateY - circle.size, // distance from top edge 171 | canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge 172 | ]; 173 | const closestEdge = edge.reduce((a, b) => Math.min(a, b)); 174 | const remapClosestEdge = parseFloat( 175 | remapValue(closestEdge, 0, 20, 0, 1).toFixed(2), 176 | ); 177 | if (remapClosestEdge > 1) { 178 | circle.alpha += 0.02; 179 | if (circle.alpha > circle.targetAlpha) 180 | circle.alpha = circle.targetAlpha; 181 | } else { 182 | circle.alpha = circle.targetAlpha * remapClosestEdge; 183 | } 184 | circle.x += circle.dx; 185 | circle.y += circle.dy; 186 | circle.translateX += 187 | (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / 188 | ease; 189 | circle.translateY += 190 | (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / 191 | ease; 192 | // circle gets out of the canvas 193 | if ( 194 | circle.x < -circle.size || 195 | circle.x > canvasSize.current.w + circle.size || 196 | circle.y < -circle.size || 197 | circle.y > canvasSize.current.h + circle.size 198 | ) { 199 | // remove the circle from the array 200 | circles.current.splice(i, 1); 201 | // create a new circle 202 | const newCircle = circleParams(); 203 | drawCircle(newCircle); 204 | // update the circle position 205 | } else { 206 | drawCircle( 207 | { 208 | ...circle, 209 | x: circle.x, 210 | y: circle.y, 211 | translateX: circle.translateX, 212 | translateY: circle.translateY, 213 | alpha: circle.alpha, 214 | }, 215 | true, 216 | ); 217 | } 218 | }); 219 | window.requestAnimationFrame(animate); 220 | }; 221 | 222 | return ( 223 | 226 | ); 227 | } 228 | -------------------------------------------------------------------------------- /components/password-field.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Eye, EyeOff } from "lucide-react"; 5 | 6 | export default function PasswordField() { 7 | const [isVisible, setIsVisible] = useState(false); 8 | 9 | const toggleVisibility = () => setIsVisible(prevState => !prevState); 10 | 11 | return ( 12 |
13 | 21 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /components/password-strength-field.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useMemo } from "react"; 4 | import { Check, X, Eye, EyeOff } from "lucide-react"; 5 | 6 | export default function PasswordStrengthField() { 7 | const [password, setPassword] = useState(""); 8 | const [isVisible, setIsVisible] = useState(false); 9 | 10 | const toggleVisibility = () => setIsVisible(!isVisible); 11 | 12 | const checkStrength = (pass: string) => { 13 | const requirements = [ 14 | { regex: /.{8,}/, text: "At least 8 characters" }, 15 | { regex: /[0-9]/, text: "At least 1 number" }, 16 | { regex: /[a-z]/, text: "At least 1 lowercase letter" }, 17 | { regex: /[A-Z]/, text: "At least 1 uppercase letter" }, 18 | { regex: /[^A-Za-z0-9]/, text: "At least 1 special character" }, 19 | ]; 20 | 21 | return requirements.map((req) => ({ 22 | met: req.regex.test(pass), 23 | text: req.text, 24 | })); 25 | }; 26 | 27 | const strength = checkStrength(password); 28 | 29 | const strengthScore = useMemo(() => { 30 | return strength.filter((req) => req.met).length; 31 | }, [strength]); 32 | 33 | const getStrengthColor = (score: number) => { 34 | if (score === 0) return "bg-gray-200"; 35 | if (score <= 2) return "bg-red-500"; 36 | if (score <= 4) return "bg-amber-500"; 37 | return "bg-emerald-500"; 38 | }; 39 | 40 | const getStrengthText = (score: number) => { 41 | if (score === 0) return "Enter a password"; 42 | if (score <= 2) return "Weak password"; 43 | if (score <= 4) return "Medium password"; 44 | return "Strong password"; 45 | }; 46 | 47 | return ( 48 |
49 | {/* Password input field with toggle visibility button */} 50 |
51 | setPassword(e.target.value)} 58 | aria-label="Password" 59 | aria-invalid={strengthScore < 5} 60 | aria-describedby="password-strength" 61 | required 62 | /> 63 | {/* Toggle password visibility button */} 64 | 78 |
79 | 80 | {/* Password strength indicator */} 81 |
89 |
93 |
94 | 95 | {/* Password strength description */} 96 |

100 | {getStrengthText(strengthScore)}. Must contain: 101 |

102 | 103 | {/* Password requirements list */} 104 |
    105 | {strength.map((req, index) => ( 106 |
  • 107 | {req.met ? ( 108 |
  • 125 | ))} 126 |
127 |
128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /components/pricing-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | interface PricingTabProps { 6 | yearly: boolean; 7 | popular?: boolean; 8 | planName: string; 9 | price: { 10 | monthly: number; 11 | yearly: number; 12 | }; 13 | planDescription: string; 14 | features: string[]; 15 | } 16 | 17 | function PricingTab(props: PricingTabProps) { 18 | return ( 19 |
20 |
21 | {props.popular && ( 22 |
23 |
24 | Most Popular 25 |
26 |
27 | )} 28 |
29 |
30 | {props.planName} 31 |
32 |
33 | 34 | $ 35 | 36 | 37 | {props.yearly ? props.price.yearly : props.price.monthly} 38 | 39 | /mo 40 |
41 |
42 | {props.planDescription} 43 |
44 | 48 | Purchase Plan 49 | 50 |
51 |
52 | Includes: 53 |
54 |
    55 | {props.features.map((feature, index) => { 56 | return ( 57 |
  • 58 | 63 | 64 | 65 | {feature} 66 |
  • 67 | ); 68 | })} 69 |
70 |
71 |
72 | ); 73 | } 74 | 75 | export default function PricingTable() { 76 | const [isAnnual, setIsAnnual] = useState(true); 77 | 78 | return ( 79 |
80 | {/* Pricing toggle */} 81 |
82 |
83 | 91 | 103 | 110 |
111 |
112 | 113 |
114 | {/* Pricing tab 1 */} 115 | 127 | 128 | {/* Pricing tab 2 */} 129 | 143 | 144 | {/* Pricing tab 3 */} 145 | 159 |
160 |
161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /components/progress-slider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useRef, useEffect } from "react"; 4 | import Image, { StaticImageData } from "next/image"; 5 | import { Transition } from "@headlessui/react"; 6 | 7 | interface Item { 8 | img: StaticImageData; 9 | desc: string; 10 | buttonIcon: StaticImageData; 11 | } 12 | 13 | export default function ProgressSlider({ items }: { items: Item[] }) { 14 | const duration: number = 5000; 15 | const itemsRef = useRef(null); 16 | const frame = useRef(0); 17 | const firstFrameTime = useRef(performance.now()); 18 | const [active, setActive] = useState(0); 19 | const [progress, setProgress] = useState(0); 20 | 21 | useEffect(() => { 22 | firstFrameTime.current = performance.now(); 23 | frame.current = requestAnimationFrame(animate); 24 | return () => { 25 | cancelAnimationFrame(frame.current); 26 | }; 27 | }, [active]); 28 | 29 | const animate = (now: number) => { 30 | let timeFraction = (now - firstFrameTime.current) / duration; 31 | if (timeFraction <= 1) { 32 | setProgress(timeFraction * 100); 33 | frame.current = requestAnimationFrame(animate); 34 | } else { 35 | timeFraction = 1; 36 | setProgress(0); 37 | setActive((active + 1) % items.length); 38 | } 39 | }; 40 | 41 | const heightFix = () => { 42 | if (itemsRef.current && itemsRef.current.parentElement) 43 | itemsRef.current.parentElement.style.height = `${itemsRef.current.clientHeight}px`; 44 | }; 45 | 46 | useEffect(() => { 47 | heightFix(); 48 | }, []); 49 | 50 | return ( 51 |
52 | {/* Item image */} 53 |
54 |
55 | {items.map((item, index) => ( 56 | heightFix()} 66 | > 67 | {item.desc} 74 | 75 | ))} 76 |
77 |
78 | {/* Buttons */} 79 |
80 | {items.map((item, index) => ( 81 | 110 | ))} 111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /components/rotating-words.tsx: -------------------------------------------------------------------------------- 1 | export default function RotatingWords() { 2 | return ( 3 |
4 | Trusted by the most innovative minds in{" "} 5 | 6 |
    7 |
  • Finance
  • 8 |
  • Tech
  • 9 |
  • AI
  • 10 |
  • Crypto
  • 11 |
  • eCommerce
  • 12 | 13 |
14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/spotlight.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useRef, useState, useEffect } from "react"; 4 | import useMousePosition from "./utils/useMousePosition"; 5 | 6 | type SpotlightProps = { 7 | children: React.ReactNode; 8 | className?: string; 9 | }; 10 | 11 | export default function Spotlight({ 12 | children, 13 | className = "", 14 | }: SpotlightProps) { 15 | const containerRef = useRef(null); 16 | const mousePosition = useMousePosition(); 17 | const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); 18 | const containerSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 }); 19 | const [boxes, setBoxes] = useState>([]); 20 | 21 | useEffect(() => { 22 | containerRef.current && 23 | setBoxes( 24 | Array.from(containerRef.current.children).map( 25 | (el) => el as HTMLElement, 26 | ), 27 | ); 28 | }, []); 29 | 30 | useEffect(() => { 31 | initContainer(); 32 | window.addEventListener("resize", initContainer); 33 | 34 | return () => { 35 | window.removeEventListener("resize", initContainer); 36 | }; 37 | }, [boxes]); 38 | 39 | useEffect(() => { 40 | onMouseMove(); 41 | }, [mousePosition]); 42 | 43 | const initContainer = () => { 44 | if (containerRef.current) { 45 | containerSize.current.w = containerRef.current.offsetWidth; 46 | containerSize.current.h = containerRef.current.offsetHeight; 47 | } 48 | }; 49 | 50 | const onMouseMove = () => { 51 | if (containerRef.current) { 52 | const rect = containerRef.current.getBoundingClientRect(); 53 | const { w, h } = containerSize.current; 54 | const x = mousePosition.x - rect.left; 55 | const y = mousePosition.y - rect.top; 56 | const inside = x < w && x > 0 && y < h && y > 0; 57 | if (inside) { 58 | mouse.current.x = x; 59 | mouse.current.y = y; 60 | boxes.forEach((box) => { 61 | const boxX = 62 | -(box.getBoundingClientRect().left - rect.left) + mouse.current.x; 63 | const boxY = 64 | -(box.getBoundingClientRect().top - rect.top) + mouse.current.y; 65 | box.style.setProperty("--mouse-x", `${boxX}px`); 66 | box.style.setProperty("--mouse-y", `${boxY}px`); 67 | }); 68 | } 69 | } 70 | }; 71 | 72 | return ( 73 |
74 | {children} 75 |
76 | ); 77 | } 78 | 79 | type SpotlightCardProps = { 80 | children: React.ReactNode; 81 | className?: string; 82 | }; 83 | 84 | export function SpotlightCard({ 85 | children, 86 | className = "", 87 | }: SpotlightCardProps) { 88 | return ( 89 |
92 | {children} 93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /components/sticky-scrolling.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useRef, useEffect, useState } from "react"; 4 | 5 | type StickyScrollingProps = { 6 | children: React.ReactNode; 7 | className?: string; 8 | }; 9 | 10 | export default function StickyScrolling({ 11 | children, 12 | className = "", 13 | }: StickyScrollingProps) { 14 | const containerRef = useRef(null); 15 | const containerProps = useRef<{ 16 | height: number; 17 | top: number; 18 | bottom: number; 19 | }>({ 20 | height: 0, 21 | top: 0, 22 | bottom: 0, 23 | }); 24 | const [sections, setSections] = useState>([]); 25 | const viewportTop = useRef(0); 26 | const activeIndex = useRef(0); 27 | const scrollValue = useRef(0); 28 | 29 | useEffect(() => { 30 | containerRef.current && 31 | setSections( 32 | Array.from(containerRef.current.querySelectorAll("section")).map( 33 | (el) => el as HTMLElement, 34 | ), 35 | ); 36 | }, []); 37 | 38 | useEffect(() => { 39 | initContainer(); 40 | handleSections(); 41 | 42 | window.addEventListener("scroll", handleSections); 43 | 44 | return () => { 45 | window.removeEventListener("scroll", handleSections); 46 | }; 47 | }, [sections]); 48 | 49 | const initContainer = () => { 50 | if (containerRef.current) { 51 | containerRef.current.style.setProperty( 52 | "--stick-items", 53 | `${sections.length + 1}00vh`, 54 | ); 55 | containerRef.current.classList.remove("hidden"); 56 | } 57 | }; 58 | 59 | const handleSections = () => { 60 | if (containerRef.current && sections) { 61 | viewportTop.current = window.scrollY; 62 | containerProps.current.height = containerRef.current.clientHeight; 63 | containerProps.current.top = containerRef.current.offsetTop; 64 | containerProps.current.bottom = 65 | containerProps.current.top + containerProps.current.height; 66 | 67 | if (containerProps.current.bottom <= viewportTop.current) { 68 | // The bottom edge of the stickContainer is above the viewport 69 | scrollValue.current = sections.length + 1; 70 | } else if (containerProps.current.top >= viewportTop.current) { 71 | // The top edge of the stickContainer is below the viewport 72 | scrollValue.current = 0; 73 | } else { 74 | // The stickContainer intersects with the viewport 75 | scrollValue.current = remapValue( 76 | viewportTop.current, 77 | containerProps.current.top, 78 | containerProps.current.bottom, 79 | 0, 80 | sections.length + 1, 81 | ); 82 | } 83 | activeIndex.current = 84 | Math.floor(scrollValue.current) >= sections.length 85 | ? sections.length - 1 86 | : Math.floor(scrollValue.current); 87 | 88 | sections.forEach((section, i) => { 89 | if (i === activeIndex.current) { 90 | section.style.setProperty("--stick-visibility", "1"); 91 | section.style.setProperty("--stick-scale", "1"); 92 | } else { 93 | section.style.setProperty("--stick-visibility", "0"); 94 | section.style.setProperty("--stick-scale", ".8"); 95 | } 96 | }); 97 | } 98 | }; 99 | 100 | const remapValue = ( 101 | value: number, 102 | start1: number, 103 | end1: number, 104 | start2: number, 105 | end2: number, 106 | ): number => { 107 | const remapped = 108 | ((value - start1) * (end2 - start2)) / (end1 - start1) + start2; 109 | return remapped > 0 ? remapped : 0; 110 | }; 111 | 112 | return ( 113 |
114 | {children} 115 |
116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /components/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 5 | import { motion, HTMLMotionProps } from "framer-motion"; 6 | import { useState } from "react"; 7 | 8 | const MotionSwitch = motion.create(SwitchPrimitives.Root); 9 | const MotionThumb = motion.create(SwitchPrimitives.Thumb); 10 | 11 | const Switch = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | HTMLMotionProps<"button"> & { 15 | checked?: boolean; 16 | onCheckedChange?: (checked: boolean) => void; 17 | } 18 | >( 19 | ( 20 | { className, checked: controlledChecked, onCheckedChange, ...props }, 21 | ref, 22 | ) => { 23 | const [internalChecked, setInternalChecked] = useState( 24 | props.defaultChecked || false, 25 | ); 26 | 27 | const isControlled = controlledChecked !== undefined; 28 | const checkedState = isControlled ? controlledChecked : internalChecked; 29 | 30 | const handleCheckedChange = (newChecked: boolean) => { 31 | if (!isControlled) { 32 | setInternalChecked(newChecked); 33 | } 34 | onCheckedChange?.(newChecked); 35 | }; 36 | 37 | const thumbVariants = { 38 | tap: { 39 | width: "24px", 40 | translateX: checkedState ? "14px" : "2px", 41 | transition: { 42 | duration: 0.15, 43 | }, 44 | }, 45 | checked: { 46 | translateX: "18px", 47 | transition: { ease: "circInOut" }, 48 | }, 49 | unchecked: { 50 | translateX: "2px", 51 | transition: { ease: "circInOut" }, 52 | }, 53 | }; 54 | 55 | return ( 56 | 65 | 70 | 71 | ); 72 | }, 73 | ); 74 | Switch.displayName = SwitchPrimitives.Root.displayName; 75 | 76 | export default Switch; 77 | -------------------------------------------------------------------------------- /components/testimonial.tsx: -------------------------------------------------------------------------------- 1 | import Image, { StaticImageData } from "next/image"; 2 | 3 | type TestimonialProps = { 4 | testimonial: { 5 | img: StaticImageData; 6 | name: string; 7 | username: string; 8 | date: string; 9 | content: string; 10 | }; 11 | }; 12 | 13 | export default function Testimonial({ testimonial }: TestimonialProps) { 14 | return ( 15 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /components/unconventional-tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, Fragment } from "react"; 4 | import Image, { StaticImageData } from "next/image"; 5 | import { TabGroup, Tab, TabList, TabPanel, TabPanels } from "@headlessui/react"; 6 | import { Transition } from "@headlessui/react"; 7 | import { Caveat } from "next/font/google"; 8 | 9 | const caveat = Caveat({ 10 | subsets: ["latin"], 11 | variable: "--font-caveat", 12 | display: "swap", 13 | }); 14 | 15 | interface Tab { 16 | title: string; 17 | img: StaticImageData; 18 | tag: string; 19 | excerpt: string; 20 | link: string; 21 | } 22 | 23 | export default function UnconventionalTabs({ tabs }: { tabs: Tab[] }) { 24 | const [selectedTab, setSelectedTab] = useState(0); 25 | 26 | return ( 27 | 28 |
29 | {/* Buttons */} 30 |
31 | 32 | {tabs.map((tab, index) => ( 33 | 34 | 39 | 40 | ))} 41 | 42 |
43 | 44 | {/* Tab panels */} 45 | 46 |
47 | {tabs.map((tab, index) => ( 48 | 49 | 50 |
51 |
52 | {tab.title} 59 |
60 |
61 |
62 |
63 |
64 | {tab.tag} 65 |
66 |

67 | {tab.title} 68 |

69 |
70 | 83 |
84 |
85 | {tab.excerpt} 86 |
87 | 95 |
96 |
97 |
98 |
99 | ))} 100 |
101 |
102 |
103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /components/utils/useMasonry.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from "react"; 2 | 3 | const useMasonry = () => { 4 | const masonryContainer = useRef(null); 5 | const [items, setItems] = useState([]); 6 | 7 | useEffect(() => { 8 | if (masonryContainer.current) { 9 | const masonryItem = Array.from(masonryContainer.current.children); 10 | setItems(masonryItem); 11 | } 12 | }, []); 13 | 14 | useEffect(() => { 15 | const handleMasonry = () => { 16 | if (!items || items.length < 1) return; 17 | let gapSize = 0; 18 | if (masonryContainer.current) { 19 | gapSize = parseInt( 20 | window 21 | .getComputedStyle(masonryContainer.current) 22 | .getPropertyValue("grid-row-gap"), 23 | ); 24 | } 25 | items.forEach((el) => { 26 | if (!(el instanceof HTMLElement)) return; 27 | let previous = el.previousSibling; 28 | while (previous) { 29 | if (previous.nodeType === 1) { 30 | el.style.marginTop = "0"; 31 | if ( 32 | previous instanceof HTMLElement && 33 | elementLeft(previous) === elementLeft(el) 34 | ) { 35 | el.style.marginTop = 36 | -(elementTop(el) - elementBottom(previous) - gapSize) + "px"; 37 | break; 38 | } 39 | } 40 | previous = previous.previousSibling; 41 | } 42 | }); 43 | }; 44 | 45 | handleMasonry(); 46 | window.addEventListener("resize", handleMasonry); 47 | return () => { 48 | window.removeEventListener("resize", handleMasonry); 49 | }; 50 | }, [items]); 51 | 52 | const elementLeft = (el: HTMLElement) => { 53 | return el.getBoundingClientRect().left; 54 | }; 55 | 56 | const elementTop = (el: HTMLElement) => { 57 | return el.getBoundingClientRect().top + window.scrollY; 58 | }; 59 | 60 | const elementBottom = (el: HTMLElement) => { 61 | return el.getBoundingClientRect().bottom + window.scrollY; 62 | }; 63 | 64 | return masonryContainer; 65 | }; 66 | 67 | export default useMasonry; 68 | -------------------------------------------------------------------------------- /components/utils/useMousePosition.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | interface MousePosition { 4 | x: number; 5 | y: number; 6 | } 7 | 8 | export default function useMousePosition(): MousePosition { 9 | const [mousePosition, setMousePosition] = useState({ 10 | x: 0, 11 | y: 0, 12 | }); 13 | 14 | useEffect(() => { 15 | const handleMouseMove = (event: MouseEvent) => { 16 | setMousePosition({ x: event.clientX, y: event.clientY }); 17 | }; 18 | 19 | window.addEventListener("mousemove", handleMouseMove); 20 | 21 | return () => { 22 | window.removeEventListener("mousemove", handleMouseMove); 23 | }; 24 | }, []); 25 | 26 | return mousePosition; 27 | } 28 | -------------------------------------------------------------------------------- /components/vertical-timeline-01.tsx: -------------------------------------------------------------------------------- 1 | import { Caveat } from "next/font/google"; 2 | 3 | const caveat = Caveat({ 4 | subsets: ["latin"], 5 | variable: "--font-caveat", 6 | display: "swap", 7 | }); 8 | 9 | interface TimelineItemProps { 10 | date: string; 11 | label: string; 12 | title: string; 13 | content: string; 14 | } 15 | 16 | export default function VerticalTimeline01({ 17 | items, 18 | }: { 19 | items: TimelineItemProps[]; 20 | }) { 21 | return ( 22 |
23 | {items.map((item, index) => ( 24 |
25 | {/* Purple label */} 26 |
27 | {item.label} 28 |
29 | {/* Time + Title */} 30 |
31 | 34 |
{item.title}
35 |
36 | {/* Description */} 37 |
{item.content}
38 |
39 | ))} 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /components/vertical-timeline-02.tsx: -------------------------------------------------------------------------------- 1 | import { Caveat } from "next/font/google"; 2 | 3 | const caveat = Caveat({ 4 | subsets: ["latin"], 5 | variable: "--font-caveat", 6 | display: "swap", 7 | }); 8 | 9 | interface TimelineItemProps { 10 | completed: boolean; 11 | deliver?: boolean; 12 | date: string; 13 | title: string; 14 | content: string; 15 | } 16 | 17 | export default function VerticalTimeline02({ 18 | items, 19 | }: { 20 | items: TimelineItemProps[]; 21 | }) { 22 | return ( 23 |
26 | {items.map((item, index) => ( 27 |
31 | {/* Icon */} 32 |
33 | {item.deliver ? ( 34 | 40 | 41 | 42 | ) : ( 43 | 49 | 53 | 54 | )} 55 |
56 | {/* Card */} 57 |
58 |
59 |
{item.title}
60 | 66 |
67 |
{item.content}
68 |
69 |
70 | ))} 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /components/vertical-timeline-03.tsx: -------------------------------------------------------------------------------- 1 | import { Caveat } from "next/font/google"; 2 | 3 | const caveat = Caveat({ 4 | subsets: ["latin"], 5 | variable: "--font-caveat", 6 | display: "swap", 7 | }); 8 | 9 | interface TimelineItemProps { 10 | date: string; 11 | author: string; 12 | type: string; 13 | content: string; 14 | } 15 | 16 | export default function VerticalTimeline03({ 17 | items, 18 | }: { 19 | items: TimelineItemProps[]; 20 | }) { 21 | const typeText = (type: string): string => { 22 | switch (type) { 23 | case "open": 24 | return "opened the request"; 25 | case "close": 26 | return "closed the request"; 27 | case "comment": 28 | return "commented the request"; 29 | default: 30 | return ""; 31 | } 32 | }; 33 | 34 | return ( 35 |
38 | {items.map((item, index) => ( 39 |
40 |
41 |
42 | {/* Icon */} 43 |
44 | {item.type === "comment" ? ( 45 | 50 | 54 | 58 | 59 | ) : ( 60 | 66 | 67 | 68 | )} 69 |
70 | {/* Date */} 71 | 74 |
75 | {/* Title */} 76 |
77 | {item.author}{" "} 78 | {typeText(item.type)} 79 |
80 |
81 | {/* Card */} 82 |
83 | {item.content} 84 |
85 |
86 | ))} 87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cruip-tutorials-next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^2.1.2", 13 | "@headlessui/tailwindcss": "^0.2.1", 14 | "@radix-ui/react-dialog": "^1.1.1", 15 | "@radix-ui/react-dropdown-menu": "^2.1.1", 16 | "@radix-ui/react-icons": "^1.3.0", 17 | "@radix-ui/react-scroll-area": "^1.1.0", 18 | "@radix-ui/react-slot": "^1.1.0", 19 | "@radix-ui/react-switch": "^1.1.0", 20 | "@radix-ui/react-visually-hidden": "^1.1.0", 21 | "@types/node": "^22.0.0", 22 | "@types/react": "^18.2.5", 23 | "@types/react-dom": "^18.2.4", 24 | "autoprefixer": "^10.4.19", 25 | "class-variance-authority": "^0.7.0", 26 | "clsx": "^2.1.1", 27 | "cmdk": "^1.0.4", 28 | "framer-motion": "^11.3.0", 29 | "lucide-react": "^0.441.0", 30 | "next": "^14.2.5", 31 | "postcss": "^8.4.24", 32 | "react": "^18.3.1", 33 | "react-dom": "^18.3.1", 34 | "react-payment-inputs": "^1.2.0", 35 | "tailwind-merge": "^2.5.2", 36 | "tailwindcss": "^3.4.6", 37 | "typescript": "^5.1.3" 38 | }, 39 | "devDependencies": { 40 | "@types/react-payment-inputs": "^1.1.4", 41 | "prettier": "^3.3.1", 42 | "prettier-plugin-tailwindcss": "^0.6.2", 43 | "tailwindcss-animate": "^1.0.7" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/Inter-Bold.ttf -------------------------------------------------------------------------------- /public/Inter-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/Inter-ExtraBold.ttf -------------------------------------------------------------------------------- /public/airbnb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/author.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/author.png -------------------------------------------------------------------------------- /public/card-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/card-01.png -------------------------------------------------------------------------------- /public/card-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/card-02.png -------------------------------------------------------------------------------- /public/card-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/card-03.png -------------------------------------------------------------------------------- /public/disney.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/dropdown-user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/dropdown-user.jpg -------------------------------------------------------------------------------- /public/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/illustration-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/illustration-01.png -------------------------------------------------------------------------------- /public/illustration-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/illustration-02.png -------------------------------------------------------------------------------- /public/illustration-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/illustration-03.png -------------------------------------------------------------------------------- /public/illustration-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/illustration-04.png -------------------------------------------------------------------------------- /public/masonry-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/masonry-01.jpg -------------------------------------------------------------------------------- /public/masonry-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/masonry-02.jpg -------------------------------------------------------------------------------- /public/masonry-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/masonry-03.jpg -------------------------------------------------------------------------------- /public/masonry-04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/masonry-04.jpg -------------------------------------------------------------------------------- /public/masonry-05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/masonry-05.jpg -------------------------------------------------------------------------------- /public/masonry-06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/masonry-06.jpg -------------------------------------------------------------------------------- /public/masonry-07.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/masonry-07.jpg -------------------------------------------------------------------------------- /public/masonry-08.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/masonry-08.jpg -------------------------------------------------------------------------------- /public/masonry-09.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/masonry-09.jpg -------------------------------------------------------------------------------- /public/modal-video-thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/modal-video-thumb.jpg -------------------------------------------------------------------------------- /public/ps-icon-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/ps-icon-02.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/ps-icon-03.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/ps-icon-04.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/ps-image-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/ps-image-01.png -------------------------------------------------------------------------------- /public/ps-image-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/ps-image-02.png -------------------------------------------------------------------------------- /public/ps-image-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/ps-image-03.png -------------------------------------------------------------------------------- /public/ps-image-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/ps-image-04.png -------------------------------------------------------------------------------- /public/quora.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/samsung.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/sass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/shape.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/social-card-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/social-card-bg.jpg -------------------------------------------------------------------------------- /public/tabs-image-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/tabs-image-01.jpg -------------------------------------------------------------------------------- /public/tabs-image-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/tabs-image-02.jpg -------------------------------------------------------------------------------- /public/tabs-image-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/tabs-image-03.jpg -------------------------------------------------------------------------------- /public/testimonial-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/testimonial-01.jpg -------------------------------------------------------------------------------- /public/testimonial-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/testimonial-02.jpg -------------------------------------------------------------------------------- /public/testimonial-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/testimonial-03.jpg -------------------------------------------------------------------------------- /public/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cruip/cruip-tutorials-next/c518af4953fdb7ec465bb003190c48b21a667b54/public/video.mp4 -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 7 | ], 8 | darkMode: ["class", "class"], 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | inter: ["var(--font-inter)", "sans-serif"], 13 | caveat: ["var(--font-caveat)", "cursive"], 14 | }, 15 | animation: { 16 | "text-slide-2": 17 | "text-slide-2 5s cubic-bezier(0.83, 0, 0.17, 1) infinite", 18 | "text-slide-3": 19 | "text-slide-3 7.5s cubic-bezier(0.83, 0, 0.17, 1) infinite", 20 | "text-slide-4": 21 | "text-slide-4 10s cubic-bezier(0.83, 0, 0.17, 1) infinite", 22 | "text-slide-5": 23 | "text-slide-5 12.5s cubic-bezier(0.83, 0, 0.17, 1) infinite", 24 | "text-slide-6": 25 | "text-slide-6 15s cubic-bezier(0.83, 0, 0.17, 1) infinite", 26 | "text-slide-7": 27 | "text-slide-7 17.5s cubic-bezier(0.83, 0, 0.17, 1) infinite", 28 | "text-slide-8": 29 | "text-slide-8 20s cubic-bezier(0.83, 0, 0.17, 1) infinite", 30 | "infinite-scroll": "infinite-scroll 25s linear infinite", 31 | }, 32 | keyframes: { 33 | "text-slide-2": { 34 | "0%, 40%": { 35 | transform: "translateY(0%)", 36 | }, 37 | "50%, 90%": { 38 | transform: "translateY(-33.33%)", 39 | }, 40 | "100%": { 41 | transform: "translateY(-66.66%)", 42 | }, 43 | }, 44 | "text-slide-3": { 45 | "0%, 26.66%": { 46 | transform: "translateY(0%)", 47 | }, 48 | "33.33%, 60%": { 49 | transform: "translateY(-25%)", 50 | }, 51 | "66.66%, 93.33%": { 52 | transform: "translateY(-50%)", 53 | }, 54 | "100%": { 55 | transform: "translateY(-75%)", 56 | }, 57 | }, 58 | "text-slide-4": { 59 | "0%, 20%": { 60 | transform: "translateY(0%)", 61 | }, 62 | "25%, 45%": { 63 | transform: "translateY(-20%)", 64 | }, 65 | "50%, 70%": { 66 | transform: "translateY(-40%)", 67 | }, 68 | "75%, 95%": { 69 | transform: "translateY(-60%)", 70 | }, 71 | "100%": { 72 | transform: "translateY(-80%)", 73 | }, 74 | }, 75 | "text-slide-5": { 76 | "0%, 16%": { 77 | transform: "translateY(0%)", 78 | }, 79 | "20%, 36%": { 80 | transform: "translateY(-16.66%)", 81 | }, 82 | "40%, 56%": { 83 | transform: "translateY(-33.33%)", 84 | }, 85 | "60%, 76%": { 86 | transform: "translateY(-50%)", 87 | }, 88 | "80%, 96%": { 89 | transform: "translateY(-66.66%)", 90 | }, 91 | "100%": { 92 | transform: "translateY(-83.33%)", 93 | }, 94 | }, 95 | "text-slide-6": { 96 | "0%, 13.33%": { 97 | transform: "translateY(0%)", 98 | }, 99 | "16.66%, 30%": { 100 | transform: "translateY(-14.28%)", 101 | }, 102 | "33.33%, 46.66%": { 103 | transform: "translateY(-28.57%)", 104 | }, 105 | "50%, 63.33%": { 106 | transform: "translateY(-42.85%)", 107 | }, 108 | "66.66%, 80%": { 109 | transform: "translateY(-57.14%)", 110 | }, 111 | "83.33%, 96.66%": { 112 | transform: "translateY(-71.42%)", 113 | }, 114 | "100%": { 115 | transform: "translateY(-85.71%)", 116 | }, 117 | }, 118 | "text-slide-7": { 119 | "0%, 11.43%": { 120 | transform: "translateY(0%)", 121 | }, 122 | "14.28%, 25.71%": { 123 | transform: "translateY(-12.5%)", 124 | }, 125 | "28.57%, 40%": { 126 | transform: "translateY(-25%)", 127 | }, 128 | "42.85%, 54.28%": { 129 | transform: "translateY(-37.5%)", 130 | }, 131 | "57.14%, 68.57%": { 132 | transform: "translateY(-50%)", 133 | }, 134 | "71.42%, 82.85%": { 135 | transform: "translateY(-62.5%)", 136 | }, 137 | "85.71%, 97.14%": { 138 | transform: "translateY(-75%)", 139 | }, 140 | "100%": { 141 | transform: "translateY(-87.5%)", 142 | }, 143 | }, 144 | "text-slide-8": { 145 | "0%, 10%": { 146 | transform: "translateY(0%)", 147 | }, 148 | "12.5%, 22.5%": { 149 | transform: "translateY(-11.11%)", 150 | }, 151 | "25%, 35%": { 152 | transform: "translateY(-22.22%)", 153 | }, 154 | "37.5%, 47.5%": { 155 | transform: "translateY(-33.33%)", 156 | }, 157 | "50%, 60%": { 158 | transform: "translateY(-44.44%)", 159 | }, 160 | "62.5%, 72.5%": { 161 | transform: "translateY(-55.55%)", 162 | }, 163 | "75%, 85%": { 164 | transform: "translateY(-66.66%)", 165 | }, 166 | "87.5%, 97.5%": { 167 | transform: "translateY(-77.77%)", 168 | }, 169 | "100%": { 170 | transform: "translateY(-88.88%)", 171 | }, 172 | }, 173 | "infinite-scroll": { 174 | from: { 175 | transform: "translateX(0)", 176 | }, 177 | to: { 178 | transform: "translateX(-100%)", 179 | }, 180 | }, 181 | }, 182 | borderRadius: { 183 | lg: "var(--radius)", 184 | md: "calc(var(--radius) - 2px)", 185 | sm: "calc(var(--radius) - 4px)", 186 | }, 187 | colors: {}, 188 | }, 189 | }, 190 | plugins: [require("@headlessui/tailwindcss"), require("tailwindcss-animate")], 191 | }; 192 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "plugins": [ 19 | { 20 | "name": "next" 21 | } 22 | ], 23 | "paths": { 24 | "@/*": ["./*"] 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | --------------------------------------------------------------------------------