Initial commit for working landing page

This commit is contained in:
Bart van der Braak 2023-06-06 03:17:35 +02:00
parent f3fecc77c5
commit 439b6eabe8
36 changed files with 9186 additions and 0 deletions

10
.editorconfig Normal file
View 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
View file

@ -0,0 +1,2 @@
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=

3
.eslintrc.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

35
.gitignore vendored Normal file
View 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
View 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
View 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
```

View 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
View 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">
&copy; {new Date().getUTCFullYear()} All rights reserved.
</p>
</div>
</div>
</footer>
</>
);
}

13
app/(landing)/page.tsx Normal file
View 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
View 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>
</>
);
}

View 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>
);
};

View 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>
);
};

View 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">
-&gt;
</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>
);
};

View 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>
);
};

View 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>
);
};

View 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
View 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>
);
};

View 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>
);
}

View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

39
package.json Normal file
View 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

File diff suppressed because it is too large Load diff

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

1
public/favicon.svg Normal file
View 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

Binary file not shown.

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

66
tailwind.config.js Normal file
View 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
View 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
View 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"]
}