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:
!MAD! 2023-05-18 17:07:03 +05:30 committed by GitHub
parent 5147e6fcdd
commit df694819fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 310 additions and 107 deletions

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

View File

@ -0,0 +1,10 @@
import Link from "next/link";
import Hero from "./Hero";
export default function HomePage() {
return (
<main className="">
<Hero />
</main>
);
}

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

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

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

View File

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

View File

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

@ -0,0 +1,4 @@
export type Message = {
type: "success" | "error" | "warning";
text: string;
};

6
frontend/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@ -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"
}
}

View File

@ -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: [],

View File

@ -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"