feat: add homepage & auth

This commit is contained in:
2024-02-23 19:36:10 +07:00
parent d579c1b7c5
commit 735fa9354b
42 changed files with 981 additions and 70 deletions
+35
View File
@@ -0,0 +1,35 @@
import Link from "~/renderer/link";
import Logo from "./logo";
const Footer = () => {
return (
<footer className="py-12 md:py-24 border-t border-white/20">
<div className="container max-w-5xl grid md:grid-cols-2 gap-x-4 gap-y-6">
<div>
<Logo size="lg" />
<p className="text-sm mt-3 text-white/80">
Create, Collaborate, and Share your web design or snippets with
ease.
</p>
</div>
<div>
<p>Links</p>
<ul className="mt-4 text-sm space-y-2 text-white/80">
<li>
<Link href="#" className="hover:text-white">
Terms and Condition
</Link>
</li>
<li>
<Link href="#" className="hover:text-white">
Privacy Policy
</Link>
</li>
</ul>
</div>
</div>
</footer>
);
};
export default Footer;
+27
View File
@@ -0,0 +1,27 @@
import React from "react";
import { cn } from "~/lib/utils";
type Props = {
size?: "sm" | "md" | "lg";
};
const Logo = ({ size = "md" }: Props) => {
const sizes: Record<typeof size, string> = {
sm: "text-lg",
md: "text-xl",
lg: "text-2xl",
};
return (
<p
className={cn(
"text-xl font-bold text-transparent bg-gradient-to-b from-gray-200 to-gray-100 bg-clip-text",
sizes[size]
)}
>
CodeShare
</p>
);
};
export default Logo;
+85
View File
@@ -0,0 +1,85 @@
import React, { ComponentProps } from "react";
import { cn } from "~/lib/utils";
import Link from "~/renderer/link";
import { Button } from "../ui/button";
import Logo from "./logo";
import { useAuth } from "~/hooks/useAuth";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { FaChevronDown } from "react-icons/fa";
import trpc from "~/lib/trpc";
const Navbar = () => {
const { user } = useAuth();
const logout = trpc.auth.logout.useMutation({
onSuccess() {
window.location.reload();
},
});
return (
<>
<div className="h-16" />
<header className="h-16 bg-background fixed z-20 top-0 left-0 w-full border-b border-white/20">
<div className="h-full container max-w-5xl flex items-center gap-3">
<Logo />
<NavMenu className="md:ml-8 flex-1">
<NavItem href="/">Home</NavItem>
{/* <NavItem href="/browse">Browse</NavItem> */}
</NavMenu>
<div className="flex items-center gap-3">
{user ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="gap-3 px-0">
<div className="size-8 rounded-full bg-white/40" />
<p>{user.name}</p>
<FaChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => logout.mutate()}>
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button>Log in</Button>
)}
</div>
</div>
</header>
</>
);
};
const NavMenu = ({ children, className }: ComponentProps<"nav">) => (
<nav className={cn("flex items-stretch h-full gap-6", className)}>
{children}
</nav>
);
type NavItemProps = {
href?: string;
children?: React.ReactNode;
};
const NavItem = ({ href, children }: NavItemProps) => {
return (
<Link
href={href}
className="flex items-center text-white/70 data-[active]:text-white hover:text-white transition-colors"
>
{children}
</Link>
);
};
export default Navbar;
+18
View File
@@ -0,0 +1,18 @@
import React from "react";
import Navbar from "../containers/navbar";
type LayoutProps = {
children: React.ReactNode;
};
const MainLayout = ({ children }: LayoutProps) => {
return (
<div>
<Navbar />
{children}
</div>
);
};
export default MainLayout;
+25 -9
View File
@@ -3,6 +3,7 @@ import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils";
import { Loader2 } from "lucide-react";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300",
@@ -14,7 +15,7 @@ const buttonVariants = cva(
destructive:
"bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/90",
outline:
"border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50",
"border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900 dark:border-white/40 dark:bg-transparent dark:hover:bg-slate-800 dark:hover:text-slate-50",
secondary:
"bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80",
ghost:
@@ -39,21 +40,36 @@ export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
href?: string;
isLoading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{ className, variant, size, type = "button", asChild = false, ...props },
ref
) => {
const Comp = asChild ? Slot : "button";
(props, ref) => {
const {
className,
variant,
size,
type = "button",
asChild = false,
disabled,
isLoading,
children,
...restProps
} = props;
const Comp = asChild ? Slot : props.href ? "a" : "button";
return (
<Comp
type={type}
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
ref={ref as any}
disabled={isLoading || disabled}
{...(restProps as any)}
>
{isLoading ? <Loader2 className="animate-spin mr-2" /> : null}
{children}
</Comp>
);
}
);
+25
View File
@@ -0,0 +1,25 @@
import React from "react";
import { cn } from "~/lib/utils";
type Props = React.ComponentProps<"div">;
const Card = ({ className, ...props }: Props) => {
return (
<div
className={cn(
"bg-background border border-white/20 rounded-lg p-4",
className
)}
{...props}
/>
);
};
export const CardTitle = ({
className,
...props
}: React.ComponentProps<"p">) => (
<p className={cn("text-2xl mb-8", className)} {...props} />
);
export default Card;
+10
View File
@@ -0,0 +1,10 @@
import React from "react";
import { cn } from "~/lib/utils";
type Props = React.ComponentProps<"hr">;
const Divider = ({ className, ...props }: Props) => {
return <hr className={cn("border-white/20", className)} {...props} />;
};
export default Divider;
+34
View File
@@ -0,0 +1,34 @@
import React from "react";
import { cn } from "~/lib/utils";
export type FormFieldProps = {
id?: string;
label?: string;
error?: string;
};
type Props = FormFieldProps & {
children?: React.ReactNode;
className?: string;
};
const FormField = ({ id, label, error, children, className }: Props) => {
return (
<div className={cn("form-field", className)}>
{label ? <FormLabel htmlFor={id}>{label}</FormLabel> : null}
{children}
{error ? <p className="text-sm mt-1 text-red-500">{error}</p> : null}
</div>
);
};
export const FormLabel = ({
className,
...props
}: React.ComponentProps<"label">) => (
<label className={cn("text-sm block mb-2", className)} {...props} />
);
export default FormField;
+63 -11
View File
@@ -1,25 +1,77 @@
import * as React from "react";
import { ForwardedRef, forwardRef, useId } from "react";
import { Controller, FieldValues, Path } from "react-hook-form";
import { useFormReturn } from "~/hooks/useForm";
import { cn } from "~/lib/utils";
import FormField, { FormFieldProps } from "./form-field";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
export type BaseInputProps = React.InputHTMLAttributes<HTMLInputElement> &
FormFieldProps & {
inputClassName?: string;
};
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
const BaseInput = forwardRef<HTMLInputElement, BaseInputProps>(
({ className, inputClassName, type, label, error, ...props }, ref) => {
const id = useId();
const input = (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300",
className
"flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/40 dark:bg-background dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300",
inputClassName
)}
ref={ref}
{...props}
/>
);
if (label || error) {
return (
<FormField
id={id}
label={label}
error={error}
className={className}
children={input}
/>
);
}
return input;
}
);
Input.displayName = "Input";
BaseInput.displayName = "BaseInput";
export { Input };
type InputProps<T extends FieldValues> = Omit<
BaseInputProps,
"form" | "name"
> & {
form?: useFormReturn<T>;
name?: Path<T>;
};
const Input = <T extends FieldValues>(props: InputProps<T>) => {
const { form, ...restProps } = props;
if (form && props.name) {
return (
<Controller
control={form.control}
name={props.name}
render={({ field, fieldState }) => (
<BaseInput
{...restProps}
{...field}
value={field.value || ""}
error={fieldState.error?.message}
/>
)}
/>
);
}
return <BaseInput {...restProps} />;
};
export { BaseInput };
export default Input;