mirror of
https://github.com/StanGirard/quivr.git
synced 2024-12-24 03:41:56 +03:00
Redesign the home page (#55)
* fix: Types * chore: Restructure * feature: Hero Section * feature: Navbar * feature: Tertiary Button * feature: Add Video * fix: Video responsive * feature: Dark Mode toggle * fix: Contrast * feature: Store dark mode in localstorage * style: Colors and bg blur
This commit is contained in:
parent
5147e6fcdd
commit
df694819fa
39
frontend/app/(home)/Hero.tsx
Normal file
39
frontend/app/(home)/Hero.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import Link from "next/link";
|
||||
import { FC } from "react";
|
||||
import Button from "../components/ui/Button";
|
||||
import { MdNorthEast } from "react-icons/md";
|
||||
|
||||
interface HeroProps {}
|
||||
|
||||
const Hero: FC<HeroProps> = ({}) => {
|
||||
return (
|
||||
<section className="relative w-full flex flex-col gap-24 items-center text-center min-h-[768px] py-12">
|
||||
<div className="flex flex-col gap-2 items-center justify-center mt-12">
|
||||
<h1 className="text-7xl font-bold max-w-xl">
|
||||
Get a Second Brain with <span className="text-primary">Quivr</span>
|
||||
</h1>
|
||||
<p className="text-base max-w-sm text-gray-500 mb-10">
|
||||
Quivr is your second brain in the cloud, designed to easily store and
|
||||
retrieve unstructured information.
|
||||
</p>
|
||||
<Link href={"/upload"}>
|
||||
<Button>Try Demo</Button>
|
||||
</Link>
|
||||
<Link target="_blank" href={"https://github.com/StanGirard/quivr/"}>
|
||||
<Button variant={"tertiary"}>
|
||||
Github <MdNorthEast />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<video
|
||||
className="rounded-md max-w-screen-lg shadow-lg dark:shadow-white/25 border dark:border-white/25 w-full"
|
||||
src="https://user-images.githubusercontent.com/19614572/238774100-80721777-2313-468f-b75e-09379f694653.mp4"
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
10
frontend/app/(home)/page.tsx
Normal file
10
frontend/app/(home)/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import Link from "next/link";
|
||||
import Hero from "./Hero";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="">
|
||||
<Hero />
|
||||
</main>
|
||||
);
|
||||
}
|
41
frontend/app/components/NavBar/DarkModeToggle.tsx
Normal file
41
frontend/app/components/NavBar/DarkModeToggle.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
import { FC, useEffect, useLayoutEffect, useState } from "react";
|
||||
import Button from "../ui/Button";
|
||||
import { MdDarkMode, MdLightMode } from "react-icons/md";
|
||||
|
||||
interface DarkModeToggleProps {}
|
||||
|
||||
const DarkModeToggle: FC<DarkModeToggleProps> = ({}) => {
|
||||
const [dark, setDark] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const isDark = localStorage.getItem("dark");
|
||||
if (isDark && isDark === "true") {
|
||||
document.body.parentElement?.classList.add("dark");
|
||||
setDark(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (dark) {
|
||||
document.body.parentElement?.classList.add("dark");
|
||||
localStorage.setItem("dark", "true");
|
||||
} else {
|
||||
document.body.parentElement?.classList.remove("dark");
|
||||
localStorage.setItem("dark", "false");
|
||||
}
|
||||
}, [dark]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
aria-label="toggle dark mode"
|
||||
className="focus:outline-none"
|
||||
onClick={() => setDark((d) => !d)}
|
||||
variant={"tertiary"}
|
||||
>
|
||||
{dark ? <MdLightMode /> : <MdDarkMode />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default DarkModeToggle;
|
46
frontend/app/components/NavBar/index.tsx
Normal file
46
frontend/app/components/NavBar/index.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import Image from "next/image";
|
||||
import { FC } from "react";
|
||||
import logo from "../../logo.png";
|
||||
import Link from "next/link";
|
||||
import Button from "../ui/Button";
|
||||
import DarkModeToggle from "./DarkModeToggle";
|
||||
|
||||
interface NavBarProps {}
|
||||
|
||||
const NavBar: FC<NavBarProps> = ({}) => {
|
||||
return (
|
||||
<header className="sticky top-0 border-b border-b-black/10 dark:border-b-white/25 bg-white/50 dark:bg-black/50 bg-opacity-0 backdrop-blur-md z-50">
|
||||
<nav className="max-w-screen-xl mx-auto py-3 flex items-center gap-8">
|
||||
<Link href={"/"} className="flex items-center gap-4">
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src={logo}
|
||||
alt="Quivr Logo"
|
||||
width={48}
|
||||
height={48}
|
||||
/>
|
||||
<h1 className="font-bold">Quivr</h1>
|
||||
</Link>
|
||||
<ul className="flex gap-4 text-sm flex-1">
|
||||
<li>
|
||||
<Link href="/#features">Features</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/chat">Chat</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/upload">Demo</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="flex">
|
||||
<Link href={"/upload"}>
|
||||
<Button variant={"secondary"}>Try Demo</Button>
|
||||
</Link>
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
50
frontend/app/components/ui/Button.tsx
Normal file
50
frontend/app/components/ui/Button.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { ButtonHTMLAttributes, FC } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ButtonVariants = cva(
|
||||
"px-8 py-3 text-sm font-medium rounded-md focus:ring ring-primary/10 outline-none flex items-center gap-2 disabled:opacity-50 transition-opacity",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary:
|
||||
"bg-black text-white dark:bg-white dark:text-black hover:bg-gray-700 dark:hover:bg-gray-200 transition-colors",
|
||||
tertiary: "text-black dark:text-white bg-transparent py-2 px-4",
|
||||
secondary:
|
||||
"border border-black dark:border-white bg-white dark:bg-black text-black dark:text-white focus:bg-black dark:focus:bg-white hover:bg-black dark:hover:bg-white hover:text-white dark:hover:text-black focus:text-white transition-colors py-2 px-4 shadow-none",
|
||||
},
|
||||
brightness: {
|
||||
dim: "",
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "primary",
|
||||
brightness: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
export interface ButtonProps
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof ButtonVariants> {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const Button: FC<ButtonProps> = ({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
brightness,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(ButtonVariants({ variant, brightness, className }))}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
@ -2,32 +2,11 @@
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
|
||||
main {
|
||||
@apply max-w-screen-xl mx-auto flex flex-col min-h-screen;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
/* Tailwind's text-white is a utility class that sets the color of the text to white.
|
||||
This will override it to use the foreground color from your root variables. */
|
||||
.text-white {
|
||||
color: rgb(var(--foreground-rgb)) !important;
|
||||
header, section {
|
||||
@apply px-5 md:px-10;
|
||||
}
|
@ -1,21 +1,28 @@
|
||||
import './globals.css'
|
||||
import { Inter } from 'next/font/google'
|
||||
import NavBar from "./components/NavBar";
|
||||
import "./globals.css";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
title: "Quivr - Get a Second Brain with Generative AI",
|
||||
description:
|
||||
"Quivr is your second brain in the cloud, designed to easily store and retrieve unstructured information.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<body
|
||||
className={`bg-white text-black dark:bg-black dark:text-white min-h-screen w-full ${inter.className}`}
|
||||
>
|
||||
<NavBar />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
BIN
frontend/app/logo.png
Normal file
BIN
frontend/app/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 277 KiB |
@ -1,28 +0,0 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
|
||||
<div className="m-4 p-6 max-w-md mx-auto bg-white rounded-xl shadow-md flex items-center space-x-4">
|
||||
<h1 className="mb-4 text-xl font-bold text-gray-900">Welcome!</h1>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<Link
|
||||
href="/chat"
|
||||
className="px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
|
||||
|
||||
Go to Chat
|
||||
|
||||
</Link>
|
||||
<Link
|
||||
href="/upload"
|
||||
className="px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
|
||||
|
||||
Go to Upload
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,49 +1,65 @@
|
||||
'use client';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import axios from 'axios';
|
||||
"use client";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import axios from "axios";
|
||||
import { Message } from "@/lib/types";
|
||||
|
||||
export default function UploadPage() {
|
||||
const [message, setMessage] = useState(null);
|
||||
const [message, setMessage] = useState<Message | null>(null);
|
||||
|
||||
const onDrop = useCallback(async (acceptedFiles) => {
|
||||
const file = acceptedFiles[0];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const response = await axios.post('http://localhost:8000/upload', formData);
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: 'File uploaded successfully: ' + JSON.stringify(response.data)
|
||||
});
|
||||
} catch (error) {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: 'Failed to upload file: ' + error.toString()
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className="flex flex-col items-center justify-center h-screen bg-gray-100"
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="mt-2 p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md flex items-center space-x-4">
|
||||
{
|
||||
isDragActive
|
||||
? <p className="text-blue-600">Drop the files here...</p>
|
||||
: <p className="text-gray-500">Drag 'n' drop some files here, or click to select files</p>
|
||||
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||
const file = acceptedFiles[0];
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
try {
|
||||
const response = await axios.post(
|
||||
"http://localhost:8000/upload",
|
||||
formData
|
||||
);
|
||||
setMessage({
|
||||
type: "success",
|
||||
text:
|
||||
"File uploaded successfully: " +
|
||||
JSON.stringify(response.data),
|
||||
});
|
||||
} catch (error: any) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Failed to upload file: " + error.toString(),
|
||||
});
|
||||
}
|
||||
</div>
|
||||
{message && (
|
||||
<div className={`mt-4 p-2 rounded ${message.type === 'success' ? 'bg-green-500' : 'bg-red-500'}`}>
|
||||
<p className="text-white">{message.text}</p>
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className="flex flex-col items-center justify-center h-screen bg-gray-100"
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="mt-2 p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md flex items-center space-x-4">
|
||||
{isDragActive ? (
|
||||
<p className="text-blue-600">Drop the files here...</p>
|
||||
) : (
|
||||
<p className="text-gray-500">
|
||||
Drag 'n' drop some files here, or click to select files
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{message && (
|
||||
<div
|
||||
className={`mt-4 p-2 rounded ${
|
||||
message.type === "success"
|
||||
? "bg-green-500"
|
||||
: "bg-red-500"
|
||||
}`}
|
||||
>
|
||||
<p className="text-white">{message.text}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
4
frontend/lib/types.ts
Normal file
4
frontend/lib/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type Message = {
|
||||
type: "success" | "error" | "warning";
|
||||
text: string;
|
||||
};
|
6
frontend/lib/utils.ts
Normal file
6
frontend/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import clsx, { ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
@ -14,6 +14,8 @@
|
||||
"@types/react-dom": "18.2.4",
|
||||
"autoprefixer": "10.4.14",
|
||||
"axios": "^1.4.0",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"eslint": "8.40.0",
|
||||
"eslint-config-next": "13.4.2",
|
||||
"next": "13.4.2",
|
||||
@ -21,7 +23,11 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"tailwind-merge": "^1.12.0",
|
||||
"tailwindcss": "3.3.2",
|
||||
"typescript": "5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-icons": "^4.8.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
@ -12,6 +13,10 @@ module.exports = {
|
||||
'gradient-conic':
|
||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||
},
|
||||
colors: {
|
||||
black: "#00121F",
|
||||
primary: "#4F46E5",
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
@ -561,11 +561,23 @@ chokidar@^3.5.3:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
class-variance-authority@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.6.0.tgz#d10df1ee148bb8efc11c17909ef1567abdc85a03"
|
||||
integrity sha512-qdRDgfjx3GRb9fpwpSvn+YaidnT7IUJNe4wt5/SWwM+PmUwJUhQRk/8zAyNro0PmVfmen2635UboTjIBXXxy5A==
|
||||
dependencies:
|
||||
clsx "1.2.1"
|
||||
|
||||
client-only@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
clsx@1.2.1, clsx@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
@ -2164,6 +2176,11 @@ react-dropzone@^14.2.3:
|
||||
file-selector "^0.6.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-icons@^4.8.0:
|
||||
version "4.8.0"
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445"
|
||||
integrity sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
@ -2442,6 +2459,11 @@ synckit@^0.8.5:
|
||||
"@pkgr/utils" "^2.3.1"
|
||||
tslib "^2.5.0"
|
||||
|
||||
tailwind-merge@^1.12.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.12.0.tgz#747d09d64a25a4864150e8930f8e436866066cc8"
|
||||
integrity sha512-Y17eDp7FtN1+JJ4OY0Bqv9OA41O+MS8c1Iyr3T6JFLnOgLg3EvcyMKZAnQ8AGyvB5Nxm3t9Xb5Mhe139m8QT/g==
|
||||
|
||||
tailwindcss@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.2.tgz#2f9e35d715fdf0bbf674d90147a0684d7054a2d3"
|
||||
|
Loading…
Reference in New Issue
Block a user