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

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