├── .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 |
26 |
27 |
28 |
29 |
34 | Home
35 |
36 |
37 |
38 |
42 | Customers
43 |
44 |
45 |
46 |
50 | Partners
51 |
52 |
53 |
54 |
58 | Team
59 |
60 |
61 |
62 |
63 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
22 |
29 |
30 |
31 | {/* Illustration #2 */}
32 |
36 |
43 |
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 |
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 |
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 |
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 |
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 |
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 |
75 |
83 |
84 | {links.map((link) => (
85 |
86 |
91 | {link.title}
92 |
93 |
94 | ))}
95 |
96 |
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 |
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 | {
30 | e.preventDefault();
31 | setAccordionOpen(!accordionOpen);
32 | }}
33 | aria-expanded={accordionOpen}
34 | aria-controls={`accordion-text-${id}`}
35 | >
36 | {title}
37 |
43 |
50 |
57 |
58 |
59 |
60 |
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 |
setBannerOpen(false)}
46 | >
47 | Close
48 |
52 |
53 |
54 |
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 |
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 |
35 |
39 |
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 |
37 |
41 |
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 |
47 |
51 |
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 |
38 |
39 |
40 | Chris Bostian
41 |
42 |
43 | @chrisbostian
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Go Pro
52 |
53 |
59 |
63 |
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 |
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 | {
104 | setActive(index);
105 | setAutorotate(false);
106 | }}
107 | >
108 | {testimonial.name} {" "}
109 |
112 | -
113 | {" "}
114 | {testimonial.role}
115 |
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 |
29 |
30 | ))}
31 |
32 |
36 | {logos.map((logo, index) => (
37 |
38 |
39 |
40 | ))}
41 |
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 |
{
41 | setModalOpen(true);
42 | }}
43 | aria-label="Watch the video"
44 | >
45 |
53 | {/* Play icon */}
54 |
60 |
67 |
71 |
72 |
73 | {/* End: Video thumbnail */}
74 |
75 |
videoRef.current?.play()}
79 | >
80 | setModalOpen(false)}>
81 | {/* Modal backdrop */}
82 |
93 | {/* End: Modal backdrop */}
94 |
95 | {/* Modal dialog */}
96 |
106 |
107 |
108 |
115 |
116 | Your browser does not support the video tag.
117 |
118 |
119 |
120 |
121 | {/* End: Modal dialog */}
122 |
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 |
224 |
225 |
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 |
29 | {isVisible ? (
30 |
31 | ) : (
32 |
33 | )}
34 |
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 |
72 | {isVisible ? (
73 |
74 | ) : (
75 |
76 | )}
77 |
78 |
79 |
80 | {/* Password strength indicator */}
81 |
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 |
113 | ) : (
114 |
115 | )}
116 |
119 | {req.text}
120 |
121 | {req.met ? " - Requirement met" : " - Requirement not met"}
122 |
123 |
124 |
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 |
87 |
90 |
91 | setIsAnnual(true)}
94 | aria-pressed={isAnnual}
95 | >
96 | Yearly{" "}
97 |
100 | -20%
101 |
102 |
103 | setIsAnnual(false)}
106 | aria-pressed={isAnnual}
107 | >
108 | Monthly
109 |
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 |
74 |
75 | ))}
76 |
77 |
78 | {/* Buttons */}
79 |
80 | {items.map((item, index) => (
81 | {
85 | setActive(index);
86 | setProgress(0);
87 | }}
88 | >
89 |
92 |
93 |
94 |
95 |
96 | {item.desc}
97 |
98 |
103 |
107 |
108 |
109 |
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 | Finance
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 |
16 |
17 |
18 |
25 |
26 |
27 | {testimonial.name}
28 |
29 | {testimonial.username ? (
30 |
38 | ) : null}
39 |
40 |
41 |
46 |
53 |
57 |
58 |
59 |
60 | {testimonial.content}
61 |
62 | {testimonial.date}
63 |
64 |
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 |
37 | {tab.title}
38 |
39 |
40 | ))}
41 |
42 |
43 |
44 | {/* Tab panels */}
45 |
46 |
47 | {tabs.map((tab, index) => (
48 |
49 |
50 |
51 |
52 |
59 |
60 |
61 |
62 |
63 |
64 | {tab.tag}
65 |
66 |
67 | {tab.title}
68 |
69 |
70 |
74 |
80 |
81 |
82 |
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 |
32 | {item.date}
33 |
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 |
63 | {!item.completed && "Exp. "}
64 | {item.date}
65 |
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 |
72 | {item.date}
73 |
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 |
--------------------------------------------------------------------------------