mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-01 11:56:25 +02:00
init
This commit is contained in:
parent
24f641347d
commit
63146aa9b7
86 changed files with 18766 additions and 0 deletions
43
web/components/Settings/EnvVarSettings.tsx
Normal file
43
web/components/Settings/EnvVarSettings.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React from "react";
|
||||
import { useToast } from "../ui/use-toast";
|
||||
export const FillEnvVariables = () => {
|
||||
const { toast } = useToast()
|
||||
return (
|
||||
<section>
|
||||
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
|
||||
SurfSense
|
||||
</a>
|
||||
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||
Required Values
|
||||
</h1>
|
||||
<form className="space-y-4 md:space-y-6" action="#">
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J URL</label>
|
||||
<input type="email" name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Username</label>
|
||||
<input type="email" name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Password</label>
|
||||
<input type="password" name="password" id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">OpenAI API Key</label>
|
||||
<input type="email" name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
<button type="submit" className="mt-4 w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Save & Proceed</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
10
web/components/homepage/Footer.tsx
Normal file
10
web/components/homepage/Footer.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export const Footer = () => {
|
||||
return (
|
||||
<footer className="mt-2 w-full md:flex overflow-y-hidden items-center justify-between gap-4 px-8 py-8 text-sm text-zinc-500 overflow-hidden text-center">
|
||||
<p>© 2024 SurfSense.net</p>
|
||||
<div className="flex gap-5 justify-around my-2">
|
||||
<a className="group/mail flex items-center" target="_blank" href="mailto:hi@dhravya.dev">Contact<svg className="group-hover/mail:opacity-100 opacity-0 transition hidden md:block" width="24px" height="24px" viewBox="-2.4 -2.4 28.80 28.80" fill="none" xmlns="http://www.w3.org/2000/svg" transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M7 17L17 7M17 7H8M17 7V16" stroke="currentColor" stroke-width="0.792" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg></a><a className="group/twit flex items-center" target="_blank" href="https://twitter.com/supermemoryai">Twitter<svg className="group-hover/twit:opacity-100 opacity-0 transition hidden md:block" width="24px" height="24px" viewBox="-2.4 -2.4 28.80 28.80" fill="none" xmlns="http://www.w3.org/2000/svg" transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M7 17L17 7M17 7H8M17 7V16" stroke="currentColor" stroke-width="0.792" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg></a><a className="group/git flex items-center" target="_blank" href="https://github.com/dhravya/supermemory">Github<svg className="group-hover/git:opacity-100 opacity-0 transition hidden md:block" width="24px" height="24px" viewBox="-2.4 -2.4 28.80 28.80" fill="none" xmlns="http://www.w3.org/2000/svg" transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M7 17L17 7M17 7H8M17 7V16" stroke="currentColor" stroke-width="0.792" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg></a>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
43
web/components/homepage/HomePage.tsx
Normal file
43
web/components/homepage/HomePage.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import React from "react";
|
||||
import { AuroraBackground } from "../ui/aurora-background";
|
||||
|
||||
import icon from "../../public/SurfSense.png"
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export function HomePage() {
|
||||
return (
|
||||
<AuroraBackground>
|
||||
<motion.div
|
||||
initial={{ opacity: 0.0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="relative flex flex-col gap-4 items-center justify-center px-4"
|
||||
>
|
||||
<div className="flex items-center mb-4 text-5xl font-semibold text-gray-900 dark:text-white">
|
||||
<Image className="w-64 h-64 rounded-full" src={icon} alt="logo" />
|
||||
</div>
|
||||
<div className="text-3xl md:text-7xl font-bold dark:text-white text-center">
|
||||
SurfSense
|
||||
</div>
|
||||
{/* <div className="text-lg font-semibold dark:text-neutral-200">Beta v0.0.1</div> */}
|
||||
<div className="font-extralight text-base md:text-4xl dark:text-neutral-200 py-4">
|
||||
A Knowledge Graph 🧠 Brain 🧠 for World Wide Web Surfers.
|
||||
</div>
|
||||
<button className="relative inline-flex h-12 overflow-hidden rounded-full p-[1px] focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50">
|
||||
<span className="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#E2CBFF_0%,#393BB2_50%,#E2CBFF_100%)]" />
|
||||
<Link href={'/signup'} className="inline-flex h-full w-full cursor-pointer items-center justify-center rounded-full bg-slate-950 px-8 py-4 text-2xl font-medium text-white backdrop-blur-3xl">
|
||||
Sign Up
|
||||
</Link>
|
||||
</button>
|
||||
</motion.div>
|
||||
</AuroraBackground>
|
||||
);
|
||||
}
|
||||
63
web/components/homepage/NavBar.tsx
Normal file
63
web/components/homepage/NavBar.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { HoveredLink, Menu, MenuItem, ProductItem } from "../ui/navbar-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThemeToggle } from "./theme-toggle";
|
||||
import Image from "next/image";
|
||||
import logo from "../../public/SurfSense.png"
|
||||
import Link from "next/link";
|
||||
|
||||
export function MainNavbar() {
|
||||
return (
|
||||
<div className="relative w-full flex items-center justify-around">
|
||||
<Navbar className="top-2 px-2" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Navbar({ className }: { className?: string }) {
|
||||
const [active, setActive] = useState<string | null>(null);
|
||||
return (
|
||||
<div
|
||||
className={cn("fixed top-10 inset-x-0 max-w-7xl mx-auto z-50", className)}
|
||||
>
|
||||
<Menu setActive={setActive}>
|
||||
<Link href={"/"} className="flex items-center text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<Image className="hidden sm:block w-8 h-8 mr-2" src={logo} alt="logo" />
|
||||
<span className="hidden md:block">SurfSense</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Link href={"/login"}>
|
||||
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
|
||||
Log In
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<Link href={"/signup"}>
|
||||
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
|
||||
Sign Up
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<Link href={"/settings"}>
|
||||
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
|
||||
Settings
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<Link href={"/chat"} className="grow">
|
||||
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
|
||||
🧠
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
|
||||
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
web/components/homepage/theme-toggle.tsx
Normal file
40
web/components/homepage/theme-toggle.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="bg-transparent" variant="ghost" size="sm">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
107
web/components/logins/LoginForm.tsx
Normal file
107
web/components/logins/LoginForm.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"use client"
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
|
||||
|
||||
export const LoginForm = () => {
|
||||
const router = useRouter();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const validateForm = () => {
|
||||
if (!username || !password) {
|
||||
setError('Username and password are required');
|
||||
return false;
|
||||
}
|
||||
setError('');
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: any) => {
|
||||
event.preventDefault();
|
||||
if (!validateForm()) return;
|
||||
setLoading(true);
|
||||
|
||||
const formDetails = new URLSearchParams();
|
||||
formDetails.append('username', username);
|
||||
formDetails.append('password', password);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL!}/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formDetails,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
window.localStorage.setItem('token', data.access_token);
|
||||
router.push('/chat');
|
||||
// navigate('/protected');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setError(errorData.detail || 'Authentication failed!');
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setError('An error occurred. Please try again later.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-8 h-8 mr-2" src={"./icon-128.png"} alt="logo" />
|
||||
SurfSense
|
||||
</a>
|
||||
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||
Sign in to your account
|
||||
</h1>
|
||||
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||
<input name="email" id="email" value={username}
|
||||
onChange={(e) => setUsername(e.target.value)} className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
<p className="text-sm font-light text-gray-500 dark:text-gray-400">
|
||||
Don’t have an account yet? <Link href={"/signup"} className="font-medium text-primary-600 hover:underline dark:text-primary-500">Sign up</Link>
|
||||
</p>
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
112
web/components/logins/RegisterForm.tsx
Normal file
112
web/components/logins/RegisterForm.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
"use client"
|
||||
import React, { FormEvent, useState } from "react";
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useToast } from "../ui/use-toast";
|
||||
import Link from "next/link";
|
||||
|
||||
export const RegisterForm = () => {
|
||||
const [captcha, setCaptcha] = useState<string | null>();
|
||||
const router = useRouter();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confpassword, setConfPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast()
|
||||
|
||||
const validateForm = () => {
|
||||
if (!username || !password || !confpassword) {
|
||||
setError('Username and password are required');
|
||||
return false;
|
||||
}
|
||||
setError('');
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (captcha) {
|
||||
if (!validateForm()) return;
|
||||
|
||||
try {
|
||||
|
||||
const toSend = {
|
||||
username: username,
|
||||
password: password,
|
||||
apisecretkey: process.env.NEXT_PUBLIC_API_SECRET_KEY!
|
||||
}
|
||||
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL!}/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(toSend),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: "Registered Successfully",
|
||||
description: "Redirecting to Login",
|
||||
})
|
||||
router.push('/login');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setError(errorData.detail || 'Authentication failed!');
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setError('An error occurred. Please try again later.');
|
||||
}
|
||||
|
||||
} else {
|
||||
setError('Recaptcha Failed');
|
||||
}
|
||||
}
|
||||
return (
|
||||
<section>
|
||||
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<div className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
|
||||
SurfSense
|
||||
</div>
|
||||
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||
Create an account
|
||||
</h1>
|
||||
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||
<input value={username}
|
||||
onChange={(e) => setUsername(e.target.value)} type="username" name="username" id="username" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||
<input value={password}
|
||||
onChange={(e) => setPassword(e.target.value)} type="password" name="password" id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
|
||||
<input value={confpassword}
|
||||
onChange={(e) => setConfPassword(e.target.value)}
|
||||
type="confirm-password" name="confpassword" id="confpassword" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<ReCAPTCHA sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!} className="mx-auto" onChange={setCaptcha} />
|
||||
<button type="submit" className="mt-4 w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"> {loading ? 'Creating...' : 'Create Account'}</button>
|
||||
<p className="text-sm font-light text-gray-500 dark:text-gray-400">
|
||||
Already have an account? <Link href={"/login"} className="font-medium text-primary-600 hover:underline dark:text-primary-500">Login here</Link>
|
||||
</p>
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
9
web/components/theme-provider.tsx
Normal file
9
web/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 { type ThemeProviderProps } from "next-themes/dist/types"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
54
web/components/ui/aurora-background.tsx
Normal file
54
web/components/ui/aurora-background.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
interface AuroraBackgroundProps extends React.HTMLProps<HTMLDivElement> {
|
||||
children: ReactNode;
|
||||
showRadialGradient?: boolean;
|
||||
}
|
||||
|
||||
export const AuroraBackground = ({
|
||||
className,
|
||||
children,
|
||||
showRadialGradient = true,
|
||||
...props
|
||||
}: AuroraBackgroundProps) => {
|
||||
return (
|
||||
<main>
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex flex-col h-[100vh] items-center justify-center bg-zinc-50 dark:bg-zinc-900 text-slate-950 transition-bg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div
|
||||
// I'm sorry but this is what peak developer performance looks like // trigger warning
|
||||
className={cn(
|
||||
`
|
||||
[--white-gradient:repeating-linear-gradient(100deg,var(--white)_0%,var(--white)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--white)_16%)]
|
||||
[--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--black)_16%)]
|
||||
[--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)]
|
||||
[background-image:var(--white-gradient),var(--aurora)]
|
||||
dark:[background-image:var(--dark-gradient),var(--aurora)]
|
||||
[background-size:300%,_200%]
|
||||
[background-position:50%_50%,50%_50%]
|
||||
filter blur-[10px] invert dark:invert-0
|
||||
after:content-[""] after:absolute after:inset-0 after:[background-image:var(--white-gradient),var(--aurora)]
|
||||
after:dark:[background-image:var(--dark-gradient),var(--aurora)]
|
||||
after:[background-size:200%,_100%]
|
||||
after:animate-aurora after:[background-attachment:fixed] after:mix-blend-difference
|
||||
pointer-events-none
|
||||
absolute -inset-[10px] opacity-50 will-change-transform`,
|
||||
|
||||
showRadialGradient &&
|
||||
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
56
web/components/ui/button.tsx
Normal file
56
web/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
11
web/components/ui/collapsible.tsx
Normal file
11
web/components/ui/collapsible.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
200
web/components/ui/dropdown-menu.tsx
Normal file
200
web/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
104
web/components/ui/lamp.tsx
Normal file
104
web/components/ui/lamp.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function LampDemo() {
|
||||
return (
|
||||
<LampContainer>
|
||||
<motion.h1
|
||||
initial={{ opacity: 0.5, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="mt-8 bg-gradient-to-br from-slate-300 to-slate-500 py-4 bg-clip-text text-center text-4xl font-medium tracking-tight text-transparent md:text-7xl"
|
||||
>
|
||||
Build lamps <br /> the right way
|
||||
</motion.h1>
|
||||
</LampContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export const LampContainer = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-slate-950 w-full rounded-md z-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="relative flex w-full flex-1 scale-y-125 items-center justify-center isolate z-0 ">
|
||||
<motion.div
|
||||
initial={{ opacity: 0.5, width: "15rem" }}
|
||||
whileInView={{ opacity: 1, width: "30rem" }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
style={{
|
||||
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
|
||||
}}
|
||||
className="absolute inset-auto right-1/2 h-56 overflow-visible w-[30rem] bg-gradient-conic from-cyan-500 via-transparent to-transparent text-white [--conic-position:from_70deg_at_center_top]"
|
||||
>
|
||||
<div className="absolute w-[100%] left-0 bg-slate-950 h-40 bottom-0 z-20 [mask-image:linear-gradient(to_top,white,transparent)]" />
|
||||
<div className="absolute w-40 h-[100%] left-0 bg-slate-950 bottom-0 z-20 [mask-image:linear-gradient(to_right,white,transparent)]" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0.5, width: "15rem" }}
|
||||
whileInView={{ opacity: 1, width: "30rem" }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
style={{
|
||||
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
|
||||
}}
|
||||
className="absolute inset-auto left-1/2 h-56 w-[30rem] bg-gradient-conic from-transparent via-transparent to-cyan-500 text-white [--conic-position:from_290deg_at_center_top]"
|
||||
>
|
||||
<div className="absolute w-40 h-[100%] right-0 bg-slate-950 bottom-0 z-20 [mask-image:linear-gradient(to_left,white,transparent)]" />
|
||||
<div className="absolute w-[100%] right-0 bg-slate-950 h-40 bottom-0 z-20 [mask-image:linear-gradient(to_top,white,transparent)]" />
|
||||
</motion.div>
|
||||
<div className="absolute top-1/2 h-48 w-full translate-y-12 scale-x-150 bg-slate-950 blur-2xl"></div>
|
||||
<div className="absolute top-1/2 z-50 h-48 w-full bg-transparent opacity-10 backdrop-blur-md"></div>
|
||||
<div className="absolute inset-auto z-50 h-36 w-[28rem] -translate-y-1/2 rounded-full bg-cyan-500 opacity-50 blur-3xl"></div>
|
||||
<motion.div
|
||||
initial={{ width: "8rem" }}
|
||||
whileInView={{ width: "16rem" }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="absolute inset-auto z-30 h-36 w-64 -translate-y-[6rem] rounded-full bg-cyan-400 blur-2xl"
|
||||
></motion.div>
|
||||
<motion.div
|
||||
initial={{ width: "15rem" }}
|
||||
whileInView={{ width: "30rem" }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="absolute inset-auto z-50 h-0.5 w-[30rem] -translate-y-[7rem] bg-cyan-400 "
|
||||
></motion.div>
|
||||
|
||||
<div className="absolute inset-auto z-40 h-44 w-full -translate-y-[12.5rem] bg-slate-950 "></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-50 flex -translate-y-80 flex-col items-center px-5">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
121
web/components/ui/navbar-menu.tsx
Normal file
121
web/components/ui/navbar-menu.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
const transition = {
|
||||
type: "spring",
|
||||
mass: 0.5,
|
||||
damping: 11.5,
|
||||
stiffness: 100,
|
||||
restDelta: 0.001,
|
||||
restSpeed: 0.001,
|
||||
};
|
||||
|
||||
export const MenuItem = ({
|
||||
setActive,
|
||||
active,
|
||||
item,
|
||||
children,
|
||||
}: {
|
||||
setActive: (item: string) => void;
|
||||
active: string | null;
|
||||
item: string;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div onMouseEnter={() => setActive(item)} className="relative ">
|
||||
<motion.p
|
||||
transition={{ duration: 0.3 }}
|
||||
className="cursor-pointer text-black hover:opacity-[0.9] dark:text-white"
|
||||
>
|
||||
{item}
|
||||
</motion.p>
|
||||
{active !== null && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.85, y: 10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
transition={transition}
|
||||
>
|
||||
{active === item && (
|
||||
<div className="absolute top-[calc(100%_+_1.2rem)] left-1/2 transform -translate-x-1/2 pt-4">
|
||||
<motion.div
|
||||
transition={transition}
|
||||
layoutId="active" // layoutId ensures smooth animation
|
||||
className="bg-white dark:bg-black backdrop-blur-sm rounded-2xl overflow-hidden border border-black/[0.2] dark:border-white/[0.2] shadow-xl"
|
||||
>
|
||||
<motion.div
|
||||
layout // layout ensures smooth animation
|
||||
className="w-max h-full p-4"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Menu = ({
|
||||
setActive,
|
||||
children,
|
||||
}: {
|
||||
setActive: (item: string | null) => void;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<nav
|
||||
onMouseLeave={() => setActive(null)} // resets the state
|
||||
className="relative rounded-full border dark:bg-black/20 dark:border-white/[0.2] bg-white/20 shadow-input flex justify-center md:justify-between space-x-4 px-10 py-4 place-items-center backdrop-blur-lg"
|
||||
>
|
||||
{children}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProductItem = ({
|
||||
title,
|
||||
description,
|
||||
href,
|
||||
src,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
src: string;
|
||||
}) => {
|
||||
return (
|
||||
<Link href={href} className="flex space-x-2">
|
||||
<Image
|
||||
src={src}
|
||||
width={140}
|
||||
height={70}
|
||||
alt={title}
|
||||
className="flex-shrink-0 rounded-md shadow-2xl"
|
||||
/>
|
||||
<div>
|
||||
<h4 className="text-xl font-bold mb-1 text-black dark:text-white">
|
||||
{title}
|
||||
</h4>
|
||||
<p className="text-neutral-700 text-sm max-w-[10rem] dark:text-neutral-300">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const HoveredLink = ({ children, ...rest }: any) => {
|
||||
return (
|
||||
<Link
|
||||
{...rest}
|
||||
className="text-neutral-700 dark:text-neutral-200 hover:text-black "
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
117
web/components/ui/table.tsx
Normal file
117
web/components/ui/table.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
129
web/components/ui/toast.tsx
Normal file
129
web/components/ui/toast.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } 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:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative 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=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
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 bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
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 right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground 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",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-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,
|
||||
}
|
||||
35
web/components/ui/toaster.tsx
Normal file
35
web/components/ui/toaster.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"use client"
|
||||
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
import { useToast } from "@/components/ui/use-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>
|
||||
)
|
||||
}
|
||||
194
web/components/ui/use-toast.ts
Normal file
194
web/components/ui/use-toast.ts
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"use client"
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
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_SAFE_INTEGER
|
||||
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 }
|
||||
Loading…
Add table
Add a link
Reference in a new issue