mirror of
https://github.com/bartvdbraak/omnidash.git
synced 2025-06-28 20:29:13 +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
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>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue