mirror of
https://github.com/bartvdbraak/omnidash.git
synced 2025-04-27 07:21:20 +00:00
Initial commit for working landing page
This commit is contained in:
parent
f3fecc77c5
commit
439b6eabe8
36 changed files with 9186 additions and 0 deletions
10
.editorconfig
Normal file
10
.editorconfig
Normal file
|
@ -0,0 +1,10 @@
|
|||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
2
.env.example
Normal file
2
.env.example
Normal file
|
@ -0,0 +1,2 @@
|
|||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
|
||||
CLERK_SECRET_KEY=
|
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
12
.prettierignore
Normal file
12
.prettierignore
Normal file
|
@ -0,0 +1,12 @@
|
|||
cache
|
||||
.cache
|
||||
package.json
|
||||
package-lock.json
|
||||
public
|
||||
CHANGELOG.md
|
||||
.yarn
|
||||
dist
|
||||
node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
50
README.md
Normal file
50
README.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
<div align="center">
|
||||
<h1 align="center">Omnidash</h1>
|
||||
<h5>Open Source Multi-client Ticket Dashboard</h5>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://omnidash.io?ref=github">omnidash.io</a>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
## Installation
|
||||
|
||||
To install the project and its dependencies, follow these steps:
|
||||
|
||||
1. Ensure you have `pnpm` installed on your system. If not, you can install it by running:
|
||||
|
||||
```sh-session
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
2. Run the following command to install the project dependencies:
|
||||
```sh-session
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
After setting up the required services, you need to set the corresponding environment variables in the `/.env` file. To do this, follow these steps:
|
||||
|
||||
1. Make a copy of the `.env.example` file:
|
||||
```sh-session
|
||||
cp .env.example .env
|
||||
```
|
||||
2. Open the `/apps/web/.env` file in a text editor and populate the values for the services mentioned above.
|
||||
|
||||
## Build
|
||||
|
||||
To build the project, execute the following command:
|
||||
|
||||
```sh-session
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
To run the project locally, use the following command:
|
||||
|
||||
```sh-session
|
||||
pnpm run dev
|
||||
```
|
30
app/(authenticated)/layout.tsx
Normal file
30
app/(authenticated)/layout.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Particles } from "@/components/landing/particles";
|
||||
import { ClerkProvider, SignIn, SignedIn, SignedOut } from "@clerk/nextjs/app-beta";
|
||||
|
||||
export default function AppLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ClerkProvider>
|
||||
<SignedIn>{children}</SignedIn>
|
||||
<SignedOut>
|
||||
<div className="flex items-center justify-center w-screen h-screen">
|
||||
<Particles className="absolute inset-0 -z-10 " />
|
||||
|
||||
<SignIn
|
||||
appearance={{
|
||||
variables: {
|
||||
colorPrimary: "#161616",
|
||||
colorText: "#161616",
|
||||
},
|
||||
}}
|
||||
afterSignInUrl={"/"}
|
||||
afterSignUpUrl={"/"}
|
||||
/>
|
||||
</div>
|
||||
</SignedOut>
|
||||
</ClerkProvider>
|
||||
);
|
||||
}
|
61
app/(landing)/layout.tsx
Normal file
61
app/(landing)/layout.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
import AOS from "aos";
|
||||
import "aos/dist/aos.css";
|
||||
|
||||
import { Header } from "@/components/landing/ui/header";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function DefaultLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
AOS.init({
|
||||
once: true,
|
||||
disable: "phone",
|
||||
duration: 1000,
|
||||
easing: "ease-out-cubic",
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<main className="grow">{children}</main>
|
||||
|
||||
<footer className="pt-24 " aria-labelledby="footer-heading">
|
||||
<h2 id="footer-heading" className="sr-only">
|
||||
Footer
|
||||
</h2>
|
||||
<div className="px-6 pb-8 mx-auto max-w-7xl lg:px-8">
|
||||
<div className="pt-8 mt-16 sm:mt-20 md:flex md:items-center md:justify-between lg:mt-24">
|
||||
<div className="flex space-x-6 md:order-2">
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://github.com/bartvdbraak/omnidash"
|
||||
className="text-gray-500 hover:text-gray-400"
|
||||
>
|
||||
<span className="sr-only">Github</span>
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-8 text-xs text-gray-400 leading-5 md:order-1 md:mt-0">
|
||||
© {new Date().getUTCFullYear()} All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
}
|
13
app/(landing)/page.tsx
Normal file
13
app/(landing)/page.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Hero } from "@/components/landing/hero";
|
||||
import { Features } from "@/components/landing/features";
|
||||
import { Cta } from "@/components/landing/cta";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="overflow-x-hidden max-w-screen">
|
||||
<Hero />
|
||||
<Features />
|
||||
<Cta />
|
||||
</div>
|
||||
);
|
||||
}
|
80
app/layout.tsx
Normal file
80
app/layout.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { Inter } from 'next/font/google';
|
||||
import LocalFont from "next/font/local";
|
||||
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import "tailwindcss/tailwind.css";
|
||||
import { ToastProvider } from "../toast-provider";
|
||||
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "Omnidash",
|
||||
template: "%s | Omnidash",
|
||||
},
|
||||
description: "Open Source Multi-client Ticket Dashboard",
|
||||
openGraph: {
|
||||
title: "Omnidash",
|
||||
description: "Open Source Multi-client Ticket Dashboard",
|
||||
url: "https://omnidash.io",
|
||||
siteName: "omnidash.io",
|
||||
locale: "en-US",
|
||||
type: "website",
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
twitter: {
|
||||
title: "Omnidash",
|
||||
card: "summary_large_image",
|
||||
},
|
||||
icons: {
|
||||
shortcut: "/favicon.png",
|
||||
},
|
||||
};
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
});
|
||||
|
||||
const calSans = LocalFont({
|
||||
src: "../public/fonts/CalSans-SemiBold.ttf",
|
||||
variable: "--font-calsans",
|
||||
});
|
||||
interface RootLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: RootLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<html
|
||||
lang="en"
|
||||
suppressHydrationWarning
|
||||
className={[inter.variable, calSans.variable].join(" ")}
|
||||
>
|
||||
<head />
|
||||
<body className="min-h-screen antialiased">
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
|
||||
<ToastProvider>{children}</ToastProvider>
|
||||
<TailwindIndicator />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
);
|
||||
}
|
47
components/landing/cta.tsx
Normal file
47
components/landing/cta.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { ArrowRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export const Cta: React.FC = () => {
|
||||
return (
|
||||
<section>
|
||||
<div className="max-w-6xl px-4 mx-auto sm:px-6">
|
||||
<div className="relative px-8 py-12 md:py-20 lg:py-32 rounded-[3rem] overflow-hidden">
|
||||
{/* Radial gradient */}
|
||||
<div
|
||||
className="absolute top-0 flex items-center justify-center w-1/3 pointer-events-none -translate-y-1/2 left-1/2 -translate-x-1/2 -z-10 aspect-square"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute inset-0 translate-z-0 bg-primary-500 rounded-full blur-[120px] opacity-70" />
|
||||
<div className="absolute w-1/4 h-1/4 translate-z-0 bg-primary-400 rounded-full blur-[40px]" />
|
||||
</div>
|
||||
{/* Blurred shape */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 opacity-50 pointer-events-none translate-y-1/2 blur-2xl -z-10"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{/* Content */}
|
||||
<div className="max-w-3xl mx-auto text-center">
|
||||
<div>
|
||||
<div className="inline-flex pb-3 font-medium text-transparent bg-clip-text bg-gradient-to-r from-primary-500 to-primary-200">
|
||||
The best way to run operations
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="pb-4 text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-zinc-200/60 via-zinc-200 to-zinc-200/60">
|
||||
Simplify your workflows
|
||||
</h2>
|
||||
<p className="mb-8 text-lg text-zinc-400">A consolidated ticket dashboard within 60 seconds.</p>
|
||||
<div>
|
||||
<Link
|
||||
className=" justify-center flex sm:inline-flex items-center whitespace-nowrap transition duration-150 ease-in-out font-medium rounded px-4 py-1.5 text-zinc-900 bg-gradient-to-r from-white/80 via-white to-white/80 hover:bg-white group"
|
||||
href="/overview"
|
||||
>
|
||||
Get Started{" "}
|
||||
<ArrowRight className="w-3 h-3 tracking-normal text-primary-500 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
104
components/landing/features.tsx
Normal file
104
components/landing/features.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import Image from "next/image";
|
||||
import GlowTop from "@/public/images/glow-top.svg";
|
||||
import { Eye, Unplug, Compass, Zap } from "lucide-react";
|
||||
|
||||
export const Features: React.FC = () => {
|
||||
const features = [
|
||||
{
|
||||
icon: Unplug,
|
||||
name: "Effortless Consolidation",
|
||||
description: "Consolidate all tickets from multiple platforms and clients effortlessly",
|
||||
},
|
||||
{
|
||||
icon: Eye,
|
||||
name: "Unparalleled Visibility",
|
||||
description: "Gain complete control and visibility over your ticketing operations",
|
||||
},
|
||||
{
|
||||
icon: Compass,
|
||||
name: "Intuitive Navigation",
|
||||
description: "Seamlessly navigate and find tickets with smart filters and advanced search",
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
name: "Enhanced Efficiency",
|
||||
description: "Maximize productivity and resource allocation in ticket management",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<section>
|
||||
<div className="relative max-w-6xl px-4 mx-auto sm:px-6">
|
||||
<div
|
||||
className="absolute inset-0 -z-10 -mx-28 rounded-t-[3rem] pointer-events-none overflow-hidden"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 -z-10">
|
||||
<Image
|
||||
src={GlowTop}
|
||||
className="max-w-none"
|
||||
width={1404}
|
||||
height={658}
|
||||
alt="Features Illustration"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-16 pb-12 md:pt-52 md:pb-20">
|
||||
<div>
|
||||
{/* Section content */}
|
||||
<div className="flex flex-col max-w-xl mx-auto md:max-w-none md:flex-row md:space-x-8 lg:space-x-16 xl:space-x-20 space-y-8 space-y-reverse md:space-y-0">
|
||||
{/* Content */}
|
||||
<div
|
||||
className="order-1 md:w-7/12 lg:w-1/2 md:order-none max-md:text-center"
|
||||
data-aos="fade-down"
|
||||
>
|
||||
{/* Content #1 */}
|
||||
<div>
|
||||
<div className="inline-flex pb-3 font-medium text-transparent bg-clip-text bg-gradient-to-r from-primary-500 to-primary-200">
|
||||
Centralized view of all tickets
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="pb-3 text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-zinc-200/60 via-zinc-200 to-zinc-200/60">
|
||||
Reduce Context Switching
|
||||
</h3>
|
||||
<p className="mb-8 text-lg text-zinc-400">
|
||||
Empower your operations teams with by consolidating all ticket information in one place. Seamlessly filter, sort, and customize ticket views to meet their unique needs.
|
||||
</p>
|
||||
<dl className="max-w-xl grid grid-cols-1 gap-4 lg:max-w-none">
|
||||
{features.map((feature) => (
|
||||
<div
|
||||
key={feature.name}
|
||||
className="px-2 py-1 rounded group hover:bg-zinc-100 duration-500"
|
||||
>
|
||||
<div className="flex items-center mb-1 space-x-2 ">
|
||||
<feature.icon className="w-4 h-4 shrink-0 text-zinc-300 group-hover:text-zinc-950 duration-500" />
|
||||
<h4 className="font-medium text-zinc-50 group-hover:text-zinc-950 duration-500">
|
||||
{feature.name}
|
||||
</h4>
|
||||
</div>
|
||||
<p className="text-sm text-left text-zinc-400 group-hover:text-zinc-950 duration-500">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="flex max-w-2xl mx-auto mt-16 md:w-5/12 lg:w-1/2 sm:mt-24 lg:ml-10 lg:mr-0 lg:mt-0 lg:max-w-none lg:flex-none xl:ml-32">
|
||||
<div className="z-10 flex-none max-w-3xl sm:max-w-5xl lg:max-w-none">
|
||||
<Image
|
||||
src="/screenshots/demo.png"
|
||||
alt="App screenshot"
|
||||
width={2432}
|
||||
height={1442}
|
||||
className="w-[76rem] z-10 rounded-xl border border-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
65
components/landing/hero.tsx
Normal file
65
components/landing/hero.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { Particles } from "./particles";
|
||||
import ReactWrapBalancer from "react-wrap-balancer";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
export const Hero: React.FC = () => {
|
||||
return (
|
||||
<section>
|
||||
<div className="relative max-w-6xl min-h-screen px-4 mx-auto sm:px-6">
|
||||
{/* Particles animation */}
|
||||
<Particles className="absolute inset-0 -z-10 " />
|
||||
|
||||
<div className="pt-32 pb-16 md:pt-52 md:pb-32">
|
||||
{/* Hero content */}
|
||||
<div className="container mx-auto text-center">
|
||||
<div className="mb-6" data-aos="fade-down">
|
||||
<div className="relative inline-flex before:absolute before:inset-0 ">
|
||||
<Link
|
||||
className="px-3 py-1 text-sm font-medium inline-flex items-center justify-center border border-transparent rounded-full text-zinc-300 hover:text-white transition duration-150 ease-in-out w-full group [background:linear-gradient(theme(colors.primary.900),_theme(colors.primary.900))_padding-box,_conic-gradient(theme(colors.primary.400),_theme(colors.primary.700)_25%,_theme(colors.primary.700)_75%,_theme(colors.primary.400)_100%)_border-box] relative before:absolute before:inset-0 before:bg-zinc-800/30 before:rounded-full before:pointer-events-none"
|
||||
href="https://github.com/bartvdbraak/omnidash"
|
||||
>
|
||||
<span className="relative inline-flex items-center">
|
||||
Omnidash is Open Source{" "}
|
||||
<span className="tracking-normal text-primary-500 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1">
|
||||
->
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<h1
|
||||
className="pb-4 font-extrabold tracking-tight text-transparent text-7xl lg:text-8xl bg-clip-text bg-gradient-to-r from-zinc-200/60 via-zinc-200 to-zinc-200/60"
|
||||
data-aos="fade-down"
|
||||
>
|
||||
<ReactWrapBalancer>One Dashboard, Countless Solutions</ReactWrapBalancer>
|
||||
</h1>
|
||||
<p className="mb-8 text-lg text-zinc-300" data-aos="fade-down" data-aos-delay="200">
|
||||
Tame ticket overload and keep your operation teams sane
|
||||
</p>
|
||||
<div
|
||||
className="flex flex-col items-center max-w-xs mx-auto gap-4 sm:max-w-none sm:justify-center sm:flex-row sm:inline-flex"
|
||||
data-aos="fade-down"
|
||||
data-aos-delay="400"
|
||||
>
|
||||
<Link
|
||||
className="w-full justify-center flex items-center whitespace-nowrap transition duration-150 ease-in-out font-medium rounded px-4 py-1.5 text-zinc-900 bg-gradient-to-r from-white/80 via-white to-white/80 hover:bg-white group"
|
||||
href="/overview"
|
||||
>
|
||||
Get Started{" "}
|
||||
<ArrowRight className="w-3 h-3 tracking-normal text-primary-500 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className="w-full transition duration-150 ease-in-out bg-opacity-25 text-zinc-200 hover:text-white bg-zinc-900 hover:bg-opacity-30"
|
||||
href="https://github.com/bartvdbraak/omnidash"
|
||||
>
|
||||
Star on GitHub
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
236
components/landing/particles.tsx
Normal file
236
components/landing/particles.tsx
Normal file
|
@ -0,0 +1,236 @@
|
|||
"use client";
|
||||
|
||||
import React, { useRef, useEffect } from "react";
|
||||
import MousePosition from "./utils/mouse-position";
|
||||
|
||||
interface ParticlesProps {
|
||||
className?: string;
|
||||
quantity?: number;
|
||||
staticity?: number;
|
||||
ease?: number;
|
||||
refresh?: boolean;
|
||||
color?: string;
|
||||
vx?: number;
|
||||
vy?: number;
|
||||
}
|
||||
function hexToRgb(hex: string): number[] {
|
||||
// Remove the "#" character from the beginning of the hex color code
|
||||
hex = hex.replace("#", "");
|
||||
|
||||
// Convert the hex color code to an integer
|
||||
const hexInt = parseInt(hex, 16);
|
||||
|
||||
// Extract the red, green, and blue components from the hex color code
|
||||
const red = (hexInt >> 16) & 255;
|
||||
const green = (hexInt >> 8) & 255;
|
||||
const blue = hexInt & 255;
|
||||
|
||||
// Return an array of the RGB values
|
||||
return [red, green, blue];
|
||||
}
|
||||
|
||||
export const Particles: React.FC<ParticlesProps> = ({
|
||||
className = "",
|
||||
quantity = 30,
|
||||
staticity = 50,
|
||||
ease = 50,
|
||||
refresh = false,
|
||||
color = "#ffffff",
|
||||
vx = 0,
|
||||
vy = 0,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
||||
const context = useRef<CanvasRenderingContext2D | null>(null);
|
||||
const circles = useRef<any[]>([]);
|
||||
const mousePosition = MousePosition();
|
||||
const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
|
||||
const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
context.current = canvasRef.current.getContext("2d");
|
||||
}
|
||||
initCanvas();
|
||||
animate();
|
||||
window.addEventListener("resize", initCanvas);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", initCanvas);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onMouseMove();
|
||||
}, [mousePosition.x, mousePosition.y]);
|
||||
|
||||
useEffect(() => {
|
||||
initCanvas();
|
||||
}, [refresh]);
|
||||
|
||||
const initCanvas = () => {
|
||||
resizeCanvas();
|
||||
drawParticles();
|
||||
};
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (canvasRef.current) {
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const { w, h } = canvasSize.current;
|
||||
const x = mousePosition.x - rect.left - w / 2;
|
||||
const y = mousePosition.y - rect.top - h / 2;
|
||||
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
|
||||
if (inside) {
|
||||
mouse.current.x = x;
|
||||
mouse.current.y = y;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
type Circle = {
|
||||
x: number;
|
||||
y: number;
|
||||
translateX: number;
|
||||
translateY: number;
|
||||
size: number;
|
||||
alpha: number;
|
||||
targetAlpha: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
magnetism: number;
|
||||
};
|
||||
|
||||
const resizeCanvas = () => {
|
||||
if (canvasContainerRef.current && canvasRef.current && context.current) {
|
||||
circles.current.length = 0;
|
||||
canvasSize.current.w = canvasContainerRef.current.offsetWidth;
|
||||
canvasSize.current.h = canvasContainerRef.current.offsetHeight;
|
||||
canvasRef.current.width = canvasSize.current.w * dpr;
|
||||
canvasRef.current.height = canvasSize.current.h * dpr;
|
||||
canvasRef.current.style.width = `${canvasSize.current.w}px`;
|
||||
canvasRef.current.style.height = `${canvasSize.current.h}px`;
|
||||
context.current.scale(dpr, dpr);
|
||||
}
|
||||
};
|
||||
|
||||
const circleParams = (): Circle => {
|
||||
const x = Math.floor(Math.random() * canvasSize.current.w);
|
||||
const y = Math.floor(Math.random() * canvasSize.current.h);
|
||||
const translateX = 0;
|
||||
const translateY = 0;
|
||||
const size = Math.floor(Math.random() * 2) + 1;
|
||||
const alpha = 0;
|
||||
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
|
||||
const dx = (Math.random() - 0.5) * 0.2;
|
||||
const dy = (Math.random() - 0.5) * 0.2;
|
||||
const magnetism = 0.1 + Math.random() * 4;
|
||||
return { x, y, translateX, translateY, size, alpha, targetAlpha, dx, dy, magnetism };
|
||||
};
|
||||
|
||||
const rgb = hexToRgb(color);
|
||||
|
||||
const drawCircle = (circle: Circle, update = false) => {
|
||||
if (context.current) {
|
||||
const { x, y, translateX, translateY, size, alpha } = circle;
|
||||
context.current.translate(translateX, translateY);
|
||||
context.current.beginPath();
|
||||
context.current.arc(x, y, size, 0, 2 * Math.PI);
|
||||
context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`;
|
||||
context.current.fill();
|
||||
context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
if (!update) {
|
||||
circles.current.push(circle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearContext = () => {
|
||||
if (context.current) {
|
||||
context.current.clearRect(0, 0, canvasSize.current.w, canvasSize.current.h);
|
||||
}
|
||||
};
|
||||
|
||||
const drawParticles = () => {
|
||||
clearContext();
|
||||
const particleCount = quantity;
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const circle = circleParams();
|
||||
drawCircle(circle);
|
||||
}
|
||||
};
|
||||
|
||||
const remapValue = (
|
||||
value: number,
|
||||
start1: number,
|
||||
end1: number,
|
||||
start2: number,
|
||||
end2: number,
|
||||
): number => {
|
||||
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||
return remapped > 0 ? remapped : 0;
|
||||
};
|
||||
|
||||
const animate = () => {
|
||||
clearContext();
|
||||
circles.current.forEach((circle: Circle, i: number) => {
|
||||
// Handle the alpha value
|
||||
const edge = [
|
||||
circle.x + circle.translateX - circle.size, // distance from left edge
|
||||
canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
|
||||
circle.y + circle.translateY - circle.size, // distance from top edge
|
||||
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
||||
];
|
||||
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
|
||||
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));
|
||||
if (remapClosestEdge > 1) {
|
||||
circle.alpha += 0.02;
|
||||
if (circle.alpha > circle.targetAlpha) {
|
||||
circle.alpha = circle.targetAlpha;
|
||||
}
|
||||
} else {
|
||||
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
||||
}
|
||||
circle.x += circle.dx + vx;
|
||||
circle.y += circle.dy + vy;
|
||||
circle.translateX +=
|
||||
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / ease;
|
||||
circle.translateY +=
|
||||
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / ease;
|
||||
// circle gets out of the canvas
|
||||
if (
|
||||
circle.x < -circle.size ||
|
||||
circle.x > canvasSize.current.w + circle.size ||
|
||||
circle.y < -circle.size ||
|
||||
circle.y > canvasSize.current.h + circle.size
|
||||
) {
|
||||
// remove the circle from the array
|
||||
circles.current.splice(i, 1);
|
||||
// create a new circle
|
||||
const newCircle = circleParams();
|
||||
drawCircle(newCircle);
|
||||
// update the circle position
|
||||
} else {
|
||||
drawCircle(
|
||||
{
|
||||
...circle,
|
||||
x: circle.x,
|
||||
y: circle.y,
|
||||
translateX: circle.translateX,
|
||||
translateY: circle.translateY,
|
||||
alpha: circle.alpha,
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
window.requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} ref={canvasContainerRef} aria-hidden="true">
|
||||
<canvas ref={canvasRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
31
components/landing/ui/header.tsx
Normal file
31
components/landing/ui/header.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Link from "next/link";
|
||||
import { Logo } from "@/components/logo";
|
||||
|
||||
export const Header: React.FC = () => {
|
||||
return (
|
||||
<header className="absolute z-30 w-full">
|
||||
<div className="max-w-6xl px-4 mx-auto sm:px-6">
|
||||
<div className="flex items-center justify-between h-16 md:h-20">
|
||||
<Link href="/" className="mr-4 shrink-0">
|
||||
<Logo className="w-8 h-8 stroke-zinc-300 hover:stroke-white duration-500" />
|
||||
</Link>
|
||||
|
||||
{/* Desktop navigation */}
|
||||
<nav className="flex grow">
|
||||
{/* Desktop sign in links */}
|
||||
<ul className="flex flex-wrap items-center justify-end grow">
|
||||
<li>
|
||||
<Link
|
||||
className="text-sm font-medium text-zinc-300 hover:text-white duration-500"
|
||||
href="/overview"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
24
components/landing/utils/mouse-position.tsx
Normal file
24
components/landing/utils/mouse-position.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
interface MousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export default function useMousePosition(): MousePosition {
|
||||
const [mousePosition, setMousePosition] = useState<MousePosition>({ x: 0, y: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
setMousePosition({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return mousePosition;
|
||||
}
|
20
components/logo.tsx
Normal file
20
components/logo.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
export const Logo: React.FC<Props> = ({ className }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 512 384"
|
||||
stroke="current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{/* <rect width="512" height="384" /> */}
|
||||
<path d="M96 64L416 64" strokeWidth="32" strokeLinecap="round" />
|
||||
<path d="M147 128L371 128" strokeWidth="32" strokeLinecap="round" />
|
||||
<path d="M221 192L381 192" strokeWidth="32" strokeLinecap="round" />
|
||||
<path d="M242 256L338 256" strokeWidth="32" strokeLinecap="round" />
|
||||
<path d="M227 320L259 320" strokeWidth="32" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
};
|
16
components/tailwind-indicator.tsx
Normal file
16
components/tailwind-indicator.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
export function TailwindIndicator() {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
return (
|
||||
<div className="fixed z-50 flex items-center justify-center w-6 h-6 p-3 font-mono text-xs text-white rounded bg-zinc-800 bottom-1 left-1">
|
||||
<div className="block sm:hidden">xs</div>
|
||||
<div className="hidden sm:block md:hidden lg:hidden xl:hidden 2xl:hidden">sm</div>
|
||||
<div className="hidden md:block lg:hidden xl:hidden 2xl:hidden">md</div>
|
||||
<div className="hidden lg:block xl:hidden 2xl:hidden">lg</div>
|
||||
<div className="hidden xl:block 2xl:hidden">xl</div>
|
||||
<div className="hidden 2xl:block">2xl</div>
|
||||
</div>
|
||||
);
|
||||
}
|
9
components/theme-provider.tsx
Normal file
9
components/theme-provider.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
121
components/ui/toast.tsx
Normal file
121
components/ui/toast.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
import * as React from "react";
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||
import { VariantProps, cva } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:top-auto sm:bottom-0 sm:right-0 sm:flex-col md:max-w-[420px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
|
||||
const toastVariants = cva(
|
||||
"data-[swipe=move]:transition-none grow-1 group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full mt-4 data-[state=closed]:slide-out-to-right-full dark:border-zinc-700 last:mt-0 sm:last:mt-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-white border-zinc-200 dark:bg-zinc-800 dark:border-zinc-700",
|
||||
destructive: "group destructive bg-red-600 text-white border-red-600 dark:border-red-600",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-zinc-200 bg-transparent px-3 text-sm font-medium transition-colors hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-red-100 group-[.destructive]:hover:border-zinc-50 group-[.destructive]:hover:bg-red-100 group-[.destructive]:hover:text-red-600 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:border-zinc-700 dark:text-zinc-100 dark:hover:bg-zinc-700 dark:hover:text-zinc-100 dark:focus:ring-zinc-400 dark:focus:ring-offset-zinc-900 dark:data-[state=open]:bg-zinc-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute top-2 right-2 rounded-md p-1 text-zinc-500 opacity-0 transition-opacity hover:text-zinc-900 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:hover:text-zinc-50",
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
};
|
34
components/ui/toaster.tsx
Normal file
34
components/ui/toaster.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
"use client";
|
||||
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast";
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && <ToastDescription>{description}</ToastDescription>}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
188
hooks/use-toast.ts
Normal file
188
hooks/use-toast.ts
Normal file
|
@ -0,0 +1,188 @@
|
|||
// Inspired by react-hot-toast library
|
||||
import * as React from "react";
|
||||
|
||||
import { ToastActionElement, type ToastProps } from "@/components/ui/toast";
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const;
|
||||
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_VALUE;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST":
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t,
|
||||
),
|
||||
};
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) {
|
||||
dismiss();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast };
|
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
10
next.config.js
Normal file
10
next.config.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
esmExternals: "loose",
|
||||
appDir: true,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
4278
package-lock.json
generated
Normal file
4278
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
39
package.json
Normal file
39
package.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "omnidash",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^4.19.0",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@types/node": "20.2.5",
|
||||
"@types/react": "18.2.8",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"aos": "^2.3.4",
|
||||
"autoprefixer": "10.4.14",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"eslint": "8.42.0",
|
||||
"eslint-config-next": "13.4.4",
|
||||
"lucide-react": "^0.236.0",
|
||||
"next": "13.4.4",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "8.4.24",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-wrap-balancer": "^0.5.0",
|
||||
"tailwind-merge": "^1.13.0",
|
||||
"tailwindcss": "3.3.2",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"typescript": "5.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/aos": "^3.0.4"
|
||||
}
|
||||
}
|
3518
pnpm-lock.yaml
Normal file
3518
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 197 B |
1
public/favicon.svg
Normal file
1
public/favicon.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tornado"><path d="M21 4H3"/><path d="M18 8H6"/><path d="M19 12H9"/><path d="M16 16h-6"/><path d="M11 20H9"/></svg>
|
After Width: | Height: | Size: 316 B |
BIN
public/fonts/CalSans-SemiBold.ttf
Normal file
BIN
public/fonts/CalSans-SemiBold.ttf
Normal file
Binary file not shown.
28
public/images/glow-top.svg
Normal file
28
public/images/glow-top.svg
Normal file
|
@ -0,0 +1,28 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1404"
|
||||
height="658">
|
||||
<defs>
|
||||
<path id="a" d="M0 0h2324v658H0z" />
|
||||
<path id="b" d="M0 0h2324v658H0z" />
|
||||
<radialGradient id="c" cx="50%" cy="0%" r="106.751%" fx="50%" fy="0%"
|
||||
gradientTransform="matrix(0 1 -.28313 0 .5 -.5)">
|
||||
<stop offset="0%" stop-color="#a1a1aa" stop-opacity="0.8" />
|
||||
<stop offset="22.35%" stop-color="#71717a" stop-opacity=".64" />
|
||||
<stop offset="100%" stop-color="#0F172A" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient id="d" x1="19.609%" x2="50%" y1="14.544%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFF" />
|
||||
<stop offset="100%" stop-color="#FFF" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<filter id="e" width="165.1%" height="170.3%" x="-32.5%" y="-35.1%"
|
||||
filterUnits="objectBoundingBox">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="50" />
|
||||
</filter>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd" transform="translate(-460)">
|
||||
<mask id="f" fill="#fff">
|
||||
<use xlink:href="#b" />
|
||||
</mask>
|
||||
<use xlink:href="#b" fill="url(#c)" />
|
||||
<path fill="url(#d)" d="m629-216 461 369-284 58z" filter="url(#e)" mask="url(#f)" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/screenshots/demo.png
Normal file
BIN
public/screenshots/demo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 172 KiB |
66
tailwind.config.js
Normal file
66
tailwind.config.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
const { fontFamily } = require("tailwindcss/defaultTheme");
|
||||
const colors = require("tailwindcss/colors");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}", "content/**/*.mdx"],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "1.5rem",
|
||||
screens: {
|
||||
"2xl": "1440px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
primary: colors.zinc,
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-inter)", ...fontFamily.sans],
|
||||
display: ["var(--font-calsans)"],
|
||||
},
|
||||
|
||||
backgroundImage: {
|
||||
"gradient-conic": "conic-gradient(var(--conic-position), var(--tw-gradient-stops))",
|
||||
"gradient-radial-top": "radial-gradient(100% 60% at 100% 0%, var(--tw-gradient-stops))",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
disco: {
|
||||
"0%": { transform: "translateY(-50%) rotate(0deg)" },
|
||||
"100%": { transform: "translateY(-50%) rotate(360deg)" },
|
||||
},
|
||||
spin: {
|
||||
from: {
|
||||
transform: "rotate(0deg)",
|
||||
},
|
||||
to: {
|
||||
transform: "rotate(360deg)",
|
||||
},
|
||||
},
|
||||
endless: {
|
||||
"0%": { transform: "translateY(0)" },
|
||||
"100%": { transform: "translateY(-245px)" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
endless: "endless 20s linear infinite",
|
||||
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
disco: "disco 1.5s linear infinite",
|
||||
"spin-forward": "spin 2s linear infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), require("@tailwindcss/forms")],
|
||||
};
|
15
toast-provider.tsx
Normal file
15
toast-provider.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
import { ToastProvider as Provider } from "@/components/ui/toast";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
|
||||
export const ToastProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<Provider>
|
||||
<Toaster />
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
};
|
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
Loading…
Reference in a new issue