mirror of
https://github.com/khairul169/code-share.git
synced 2025-04-29 17:19:37 +07:00
feat: add homepage & auth
This commit is contained in:
parent
d579c1b7c5
commit
735fa9354b
3
.env
3
.env
@ -1 +1,2 @@
|
|||||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
BASE_URL=http://localhost:3000
|
||||||
|
JWT_KEY=test123
|
||||||
|
@ -1 +1,2 @@
|
|||||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
BASE_URL=http://localhost:3000
|
||||||
|
JWT_KEY=test123
|
||||||
|
35
components/containers/footer.tsx
Normal file
35
components/containers/footer.tsx
Normal 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
components/containers/logo.tsx
Normal file
27
components/containers/logo.tsx
Normal 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
components/containers/navbar.tsx
Normal file
85
components/containers/navbar.tsx
Normal 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
components/layout/main-layout.tsx
Normal file
18
components/layout/main-layout.tsx
Normal 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;
|
@ -3,6 +3,7 @@ import { Slot } from "@radix-ui/react-slot";
|
|||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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",
|
"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:
|
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",
|
"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:
|
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:
|
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",
|
"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:
|
ghost:
|
||||||
@ -39,21 +40,36 @@ export interface ButtonProps
|
|||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
|
href?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
(
|
(props, ref) => {
|
||||||
{ className, variant, size, type = "button", asChild = false, ...props },
|
const {
|
||||||
ref
|
className,
|
||||||
) => {
|
variant,
|
||||||
const Comp = asChild ? Slot : "button";
|
size,
|
||||||
|
type = "button",
|
||||||
|
asChild = false,
|
||||||
|
disabled,
|
||||||
|
isLoading,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
} = props;
|
||||||
|
const Comp = asChild ? Slot : props.href ? "a" : "button";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
ref={ref}
|
ref={ref as any}
|
||||||
{...props}
|
disabled={isLoading || disabled}
|
||||||
/>
|
{...(restProps as any)}
|
||||||
|
>
|
||||||
|
{isLoading ? <Loader2 className="animate-spin mr-2" /> : null}
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
25
components/ui/card.tsx
Normal file
25
components/ui/card.tsx
Normal 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
components/ui/divider.tsx
Normal file
10
components/ui/divider.tsx
Normal 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
components/ui/form-field.tsx
Normal file
34
components/ui/form-field.tsx
Normal 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;
|
@ -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 { cn } from "~/lib/utils";
|
||||||
|
import FormField, { FormFieldProps } from "./form-field";
|
||||||
|
|
||||||
export interface InputProps
|
export type BaseInputProps = React.InputHTMLAttributes<HTMLInputElement> &
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
FormFieldProps & {
|
||||||
|
inputClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const BaseInput = forwardRef<HTMLInputElement, BaseInputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, inputClassName, type, label, error, ...props }, ref) => {
|
||||||
return (
|
const id = useId();
|
||||||
|
|
||||||
|
const input = (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
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",
|
"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",
|
||||||
className
|
inputClassName
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...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;
|
||||||
|
7
hooks/useAuth.ts
Normal file
7
hooks/useAuth.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { usePageContext } from "~/renderer/context";
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const { user } = usePageContext();
|
||||||
|
|
||||||
|
return { user, isLoggedIn: user != null };
|
||||||
|
};
|
@ -20,3 +20,7 @@ export const useForm = <T extends FieldValues>(
|
|||||||
|
|
||||||
return form;
|
return form;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type useFormReturn<T extends FieldValues> = ReturnType<
|
||||||
|
typeof useForm<T>
|
||||||
|
>;
|
||||||
|
@ -25,3 +25,11 @@ export function getPreviewUrl(
|
|||||||
typeof file === "string" ? file : file.path
|
typeof file === "string" ? file : file.path
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ucfirst = (str: string) => {
|
||||||
|
return str.charAt(0).toUpperCase() + str.substring(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ucwords = (str: string) => {
|
||||||
|
return str.split(" ").map(ucfirst).join(" ");
|
||||||
|
};
|
||||||
|
@ -22,8 +22,10 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@swc/cli": "^0.3.9",
|
"@swc/cli": "^0.3.9",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/cookie-parser": "^1.4.6",
|
"@types/cookie-parser": "^1.4.6",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/node": "^20.11.19",
|
"@types/node": "^20.11.19",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "^0.2.3",
|
||||||
"@types/react": "^18.2.57",
|
"@types/react": "^18.2.57",
|
||||||
@ -42,6 +44,7 @@
|
|||||||
"@codemirror/lang-javascript": "^6.2.1",
|
"@codemirror/lang-javascript": "^6.2.1",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@emmetio/codemirror6-plugin": "^0.3.0",
|
"@emmetio/codemirror6-plugin": "^0.3.0",
|
||||||
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
@ -63,9 +66,11 @@
|
|||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"cssnano": "^6.0.3",
|
"cssnano": "^6.0.3",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
"drizzle-orm": "^0.29.3",
|
"drizzle-orm": "^0.29.3",
|
||||||
"drizzle-zod": "^0.5.1",
|
"drizzle-zod": "^0.5.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.331.0",
|
"lucide-react": "^0.331.0",
|
||||||
"mime": "^4.0.1",
|
"mime": "^4.0.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
3
pages/auth/+Layout.tsx
Normal file
3
pages/auth/+Layout.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import MainLayout from "~/components/layout/main-layout";
|
||||||
|
|
||||||
|
export { MainLayout as Layout };
|
61
pages/auth/login/+Page.tsx
Normal file
61
pages/auth/login/+Page.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import Card, { CardTitle } from "~/components/ui/card";
|
||||||
|
import Divider from "~/components/ui/divider";
|
||||||
|
import Input from "~/components/ui/input";
|
||||||
|
import { useForm } from "~/hooks/useForm";
|
||||||
|
import trpc from "~/lib/trpc";
|
||||||
|
import { useSearchParams } from "~/renderer/hooks";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialValue: z.infer<typeof schema> = {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const form = useForm(schema, initialValue);
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const login = trpc.auth.login.useMutation({
|
||||||
|
onSuccess() {
|
||||||
|
const prevPage = searchParams.get("return");
|
||||||
|
window.location.href = prevPage || "/";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((values) => {
|
||||||
|
login.mutate(values);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="container max-w-xl min-h-[80dvh] flex flex-col items-center justify-center py-8 md:py-16">
|
||||||
|
<Card className="w-full md:p-8">
|
||||||
|
<CardTitle>Log in</CardTitle>
|
||||||
|
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Input form={form} name="email" label="Email Address" />
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider className="mt-8 mb-4" />
|
||||||
|
|
||||||
|
<Button type="submit" isLoading={login.isPending}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
3
pages/index/+Layout.tsx
Normal file
3
pages/index/+Layout.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import MainLayout from "~/components/layout/main-layout";
|
||||||
|
|
||||||
|
export { MainLayout as Layout };
|
@ -1,20 +1,61 @@
|
|||||||
|
import { Button } from "~/components/ui/button";
|
||||||
import type { Data } from "./+data";
|
import type { Data } from "./+data";
|
||||||
import { useData } from "~/renderer/hooks";
|
import { useData } from "~/renderer/hooks";
|
||||||
import Link from "~/renderer/link";
|
import Link from "~/renderer/link";
|
||||||
|
import Footer from "~/components/containers/footer";
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
const { projects } = useData<Data>();
|
const { projects } = useData<Data>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<h1>Posts</h1>
|
<main>
|
||||||
|
<section className="container py-8 md:py-16 min-h-dvh md:min-h-[80vh] flex items-center justify-center flex-col text-center">
|
||||||
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-medium text-transparent bg-gradient-to-b from-gray-200 to-gray-100 bg-clip-text">
|
||||||
|
Create and Share Web Snippets
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg mt-8 md:mt-12 text-gray-300">
|
||||||
|
Collaborate with Ease, Share with the World
|
||||||
|
</p>
|
||||||
|
|
||||||
{projects.map((project: any) => (
|
<div className="flex flex-col sm:flex-row gap-3 items-center mt-8 md:mt-12">
|
||||||
<Link key={project.id} href={`/${project.slug}`}>
|
<Button href="/get-started" size="lg">
|
||||||
{project.title}
|
Getting Started
|
||||||
|
</Button>
|
||||||
|
<Button href="#browse" size="lg" variant="outline">
|
||||||
|
Browse Projects
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="browse" className="container max-w-5xl py-8 md:py-16">
|
||||||
|
<h2 className="text-4xl font-medium border-b pb-3 mb-8 border-white/20">
|
||||||
|
Community Projects
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid sm:grid-cols-2 md:grid-cols-3 gap-3 md:gap-4">
|
||||||
|
{projects.map((project) => (
|
||||||
|
<Link
|
||||||
|
key={project.id}
|
||||||
|
href={`/${project.slug}`}
|
||||||
|
className="border border-white/20 hover:border-white/40 rounded-lg transition-colors overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="w-full aspect-[3/2] bg-gray-900"></div>
|
||||||
|
<div className="py-2 px-3 flex items-center gap-3">
|
||||||
|
<div className="size-8 rounded-full bg-white/80"></div>
|
||||||
|
<div>
|
||||||
|
<p className="text-md truncate">{project.title}</p>
|
||||||
|
<p className="text-xs truncate">{project.user.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
143
pages/index/get-started/+Page.tsx
Normal file
143
pages/index/get-started/+Page.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import Card, { CardTitle } from "~/components/ui/card";
|
||||||
|
import { FormLabel } from "~/components/ui/form-field";
|
||||||
|
import Input from "~/components/ui/input";
|
||||||
|
import { useData } from "~/renderer/hooks";
|
||||||
|
import { Data } from "./+data";
|
||||||
|
import { useForm } from "~/hooks/useForm";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import { navigate } from "vike/client/router";
|
||||||
|
import Divider from "~/components/ui/divider";
|
||||||
|
import trpc from "~/lib/trpc";
|
||||||
|
import { useAuth } from "~/hooks/useAuth";
|
||||||
|
import { usePageContext } from "~/renderer/context";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
forkFromId: z.number(),
|
||||||
|
title: z.string(),
|
||||||
|
user: z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(3),
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(6),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialValue: z.infer<typeof schema> = {
|
||||||
|
forkFromId: 0,
|
||||||
|
title: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const GetStartedPage = () => {
|
||||||
|
const { presets } = useData<Data>();
|
||||||
|
const { isLoggedIn } = useAuth();
|
||||||
|
const ctx = usePageContext();
|
||||||
|
|
||||||
|
const form = useForm(schema, initialValue);
|
||||||
|
const create = trpc.project.create.useMutation({
|
||||||
|
onSuccess(data) {
|
||||||
|
navigate(`/${data.slug}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((values) => {
|
||||||
|
create.mutate(values);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container max-w-3xl min-h-[80dvh] flex flex-col items-center justify-center py-8 md:py-16">
|
||||||
|
<Card className="w-full md:p-8">
|
||||||
|
<CardTitle>Create New Project</CardTitle>
|
||||||
|
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<FormLabel>Select Preset</FormLabel>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="forkFromId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex md:grid md:grid-cols-3 gap-4 overflow-x-auto md:overflow-x-hidden">
|
||||||
|
{presets.map((preset) => (
|
||||||
|
<Button
|
||||||
|
key={preset.projectId}
|
||||||
|
variant={
|
||||||
|
field.value === preset.projectId ? "default" : "outline"
|
||||||
|
}
|
||||||
|
className="flex py-16 border border-white/40 shrink-0 w-[160px] md:w-auto"
|
||||||
|
onClick={() => field.onChange(preset.projectId)}
|
||||||
|
>
|
||||||
|
<p className="text-wrap">{preset.title}</p>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
name="title"
|
||||||
|
label="Title"
|
||||||
|
placeholder="Optional"
|
||||||
|
className="mt-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isLoggedIn ? (
|
||||||
|
<div className="mt-8">
|
||||||
|
<p className="text-lg">Account detail</p>
|
||||||
|
<Divider className="mt-2 mb-4" />
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-x-2 gap-y-4">
|
||||||
|
<div className="order-1 md:order-2 flex flex-col items-center justify-center gap-3 py-4">
|
||||||
|
<p className="text-center">Already have an account?</p>
|
||||||
|
<Button
|
||||||
|
href={`/auth/login?return=${encodeURI(ctx.urlPathname)}`}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Log in now
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="order-2 md:order-1 space-y-3">
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
name="user.name"
|
||||||
|
label="Full Name"
|
||||||
|
placeholder="John Doe"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
name="user.email"
|
||||||
|
label="Email Address"
|
||||||
|
placeholder="john.doe@mail.com"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
type="password"
|
||||||
|
name="user.password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="P@ssw0rd123!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Divider className="my-6" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="lg"
|
||||||
|
isLoading={create.isPending}
|
||||||
|
className="w-full md:w-[160px]"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GetStartedPage;
|
11
pages/index/get-started/+data.ts
Normal file
11
pages/index/get-started/+data.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { PageContext } from "vike/types";
|
||||||
|
import trpcServer from "~/server/api/trpc/trpc";
|
||||||
|
|
||||||
|
export const data = async (ctx: PageContext) => {
|
||||||
|
const trpc = await trpcServer(ctx);
|
||||||
|
const presets = await trpc.project.getTemplates();
|
||||||
|
|
||||||
|
return { presets };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Data = Awaited<ReturnType<typeof data>>;
|
@ -5,7 +5,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "../../../../components/ui/dialog";
|
} from "../../../../components/ui/dialog";
|
||||||
import { UseDiscloseReturn } from "~/hooks/useDisclose";
|
import { UseDiscloseReturn } from "~/hooks/useDisclose";
|
||||||
import { Input } from "../../../../components/ui/input";
|
import { BaseInput } from "../../../../components/ui/input";
|
||||||
import { Button } from "../../../../components/ui/button";
|
import { Button } from "../../../../components/ui/button";
|
||||||
import { useForm } from "~/hooks/useForm";
|
import { useForm } from "~/hooks/useForm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -76,7 +76,7 @@ const CreateFileDialog = ({ disclose, onSuccess }: Props) => {
|
|||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<FormErrorMessage form={form} />
|
<FormErrorMessage form={form} />
|
||||||
|
|
||||||
<Input
|
<BaseInput
|
||||||
placeholder={isDir ? "Directory Name" : "Filename"}
|
placeholder={isDir ? "Directory Name" : "Filename"}
|
||||||
autoFocus
|
autoFocus
|
||||||
{...form.register("filename")}
|
{...form.register("filename")}
|
||||||
|
@ -3,8 +3,7 @@ import Panel from "~/components/ui/panel";
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { useProjectContext } from "../context/project";
|
import { useProjectContext } from "../context/project";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { FaEllipsisV, FaRedo } from "react-icons/fa";
|
import { FaRedo } from "react-icons/fa";
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import { previewStore } from "../stores/web-preview";
|
import { previewStore } from "../stores/web-preview";
|
||||||
|
|
||||||
type WebPreviewProps = {
|
type WebPreviewProps = {
|
||||||
|
106
pnpm-lock.yaml
generated
106
pnpm-lock.yaml
generated
@ -20,6 +20,9 @@ dependencies:
|
|||||||
'@emmetio/codemirror6-plugin':
|
'@emmetio/codemirror6-plugin':
|
||||||
specifier: ^0.3.0
|
specifier: ^0.3.0
|
||||||
version: 0.3.0(@codemirror/autocomplete@6.12.0)(@codemirror/commands@6.3.3)(@codemirror/highlight@0.19.8)(@codemirror/history@0.19.2)(@codemirror/lang-css@6.2.1)(@codemirror/lang-html@6.4.8)(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.24.1)(@lezer/common@1.2.1)
|
version: 0.3.0(@codemirror/autocomplete@6.12.0)(@codemirror/commands@6.3.3)(@codemirror/highlight@0.19.8)(@codemirror/history@0.19.2)(@codemirror/lang-css@6.2.1)(@codemirror/lang-html@6.4.8)(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.24.1)(@lezer/common@1.2.1)
|
||||||
|
'@faker-js/faker':
|
||||||
|
specifier: ^8.4.1
|
||||||
|
version: 8.4.1
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^3.3.4
|
specifier: ^3.3.4
|
||||||
version: 3.3.4(react-hook-form@7.50.1)
|
version: 3.3.4(react-hook-form@7.50.1)
|
||||||
@ -83,6 +86,9 @@ dependencies:
|
|||||||
cssnano:
|
cssnano:
|
||||||
specifier: ^6.0.3
|
specifier: ^6.0.3
|
||||||
version: 6.0.3(postcss@8.4.35)
|
version: 6.0.3(postcss@8.4.35)
|
||||||
|
dotenv:
|
||||||
|
specifier: ^16.4.5
|
||||||
|
version: 16.4.5
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.29.3
|
specifier: ^0.29.3
|
||||||
version: 0.29.3(@types/react@18.2.57)(better-sqlite3@9.4.2)(react@18.2.0)
|
version: 0.29.3(@types/react@18.2.57)(better-sqlite3@9.4.2)(react@18.2.0)
|
||||||
@ -92,6 +98,9 @@ dependencies:
|
|||||||
express:
|
express:
|
||||||
specifier: ^4.18.2
|
specifier: ^4.18.2
|
||||||
version: 4.18.2
|
version: 4.18.2
|
||||||
|
jsonwebtoken:
|
||||||
|
specifier: ^9.0.2
|
||||||
|
version: 9.0.2
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.331.0
|
specifier: ^0.331.0
|
||||||
version: 0.331.0(react@18.2.0)
|
version: 0.331.0(react@18.2.0)
|
||||||
@ -145,12 +154,18 @@ devDependencies:
|
|||||||
'@swc/cli':
|
'@swc/cli':
|
||||||
specifier: ^0.3.9
|
specifier: ^0.3.9
|
||||||
version: 0.3.9(@swc/core@1.4.2)
|
version: 0.3.9(@swc/core@1.4.2)
|
||||||
|
'@types/bcrypt':
|
||||||
|
specifier: ^5.0.2
|
||||||
|
version: 5.0.2
|
||||||
'@types/cookie-parser':
|
'@types/cookie-parser':
|
||||||
specifier: ^1.4.6
|
specifier: ^1.4.6
|
||||||
version: 1.4.6
|
version: 1.4.6
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
'@types/jsonwebtoken':
|
||||||
|
specifier: ^9.0.5
|
||||||
|
version: 9.0.5
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.11.19
|
specifier: ^20.11.19
|
||||||
version: 20.11.19
|
version: 20.11.19
|
||||||
@ -1165,6 +1180,11 @@ packages:
|
|||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@faker-js/faker@8.4.1:
|
||||||
|
resolution: {integrity: sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==}
|
||||||
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@floating-ui/core@1.6.0:
|
/@floating-ui/core@1.6.0:
|
||||||
resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==}
|
resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2202,6 +2222,12 @@ packages:
|
|||||||
'@babel/types': 7.23.9
|
'@babel/types': 7.23.9
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/bcrypt@5.0.2:
|
||||||
|
resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.11.19
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/body-parser@1.19.5:
|
/@types/body-parser@1.19.5:
|
||||||
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
|
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2259,6 +2285,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/jsonwebtoken@9.0.5:
|
||||||
|
resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.11.19
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/keyv@3.1.4:
|
/@types/keyv@3.1.4:
|
||||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2674,6 +2706,10 @@ packages:
|
|||||||
node-releases: 2.0.14
|
node-releases: 2.0.14
|
||||||
update-browserslist-db: 1.0.13(browserslist@4.23.0)
|
update-browserslist-db: 1.0.13(browserslist@4.23.0)
|
||||||
|
|
||||||
|
/buffer-equal-constant-time@1.0.1:
|
||||||
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/buffer-from@1.1.2:
|
/buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
@ -3213,6 +3249,11 @@ packages:
|
|||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/dotenv@16.4.5:
|
||||||
|
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dreamopt@0.8.0:
|
/dreamopt@0.8.0:
|
||||||
resolution: {integrity: sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==}
|
resolution: {integrity: sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
@ -3331,6 +3372,12 @@ packages:
|
|||||||
/eastasianwidth@0.2.0:
|
/eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
/ecdsa-sig-formatter@1.0.11:
|
||||||
|
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/ee-first@1.1.1:
|
/ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -4145,6 +4192,37 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/jsonwebtoken@9.0.2:
|
||||||
|
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||||
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
jws: 3.2.2
|
||||||
|
lodash.includes: 4.3.0
|
||||||
|
lodash.isboolean: 3.0.3
|
||||||
|
lodash.isinteger: 4.0.4
|
||||||
|
lodash.isnumber: 3.0.3
|
||||||
|
lodash.isplainobject: 4.0.6
|
||||||
|
lodash.isstring: 4.0.1
|
||||||
|
lodash.once: 4.1.1
|
||||||
|
ms: 2.1.3
|
||||||
|
semver: 7.6.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/jwa@1.4.1:
|
||||||
|
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: 1.0.1
|
||||||
|
ecdsa-sig-formatter: 1.0.11
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/jws@3.2.2:
|
||||||
|
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||||
|
dependencies:
|
||||||
|
jwa: 1.4.1
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/keyv@4.5.4:
|
/keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4178,10 +4256,38 @@ packages:
|
|||||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/lodash.includes@4.3.0:
|
||||||
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isboolean@3.0.3:
|
||||||
|
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isinteger@4.0.4:
|
||||||
|
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isnumber@3.0.3:
|
||||||
|
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isplainobject@4.0.6:
|
||||||
|
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isstring@4.0.1:
|
||||||
|
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.memoize@4.1.2:
|
/lodash.memoize@4.1.2:
|
||||||
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/lodash.once@4.1.1:
|
||||||
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.throttle@4.1.1:
|
/lodash.throttle@4.1.1:
|
||||||
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
|
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -2,7 +2,7 @@ import type { Config } from "vike/types";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
clientRouting: true,
|
clientRouting: true,
|
||||||
passToClient: ["routeParams", "cookies"],
|
passToClient: ["routeParams", "cookies", "user"],
|
||||||
meta: {
|
meta: {
|
||||||
title: {
|
title: {
|
||||||
env: { server: true, client: true },
|
env: { server: true, client: true },
|
||||||
@ -10,6 +10,9 @@ export default {
|
|||||||
description: {
|
description: {
|
||||||
env: { server: true },
|
env: { server: true },
|
||||||
},
|
},
|
||||||
|
Layout: {
|
||||||
|
env: { server: true, client: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
hydrationCanBeAborted: true,
|
hydrationCanBeAborted: true,
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { getPageMetadata } from "./utils";
|
import { getPageMetadata } from "./utils";
|
||||||
import type { OnRenderClientAsync } from "vike/types";
|
import type { OnRenderClientAsync } from "vike/types";
|
||||||
import Layout from "./layout";
|
import Layout from "./app";
|
||||||
|
|
||||||
let root: ReactDOM.Root;
|
let root: ReactDOM.Root;
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import ReactDOMServer from "react-dom/server";
|
|||||||
import { escapeInject, dangerouslySkipEscape } from "vike/server";
|
import { escapeInject, dangerouslySkipEscape } from "vike/server";
|
||||||
import type { OnRenderHtmlAsync } from "vike/types";
|
import type { OnRenderHtmlAsync } from "vike/types";
|
||||||
import { getPageMetadata } from "./utils";
|
import { getPageMetadata } from "./utils";
|
||||||
import Layout from "./layout";
|
import App from "./app";
|
||||||
|
|
||||||
export const onRenderHtml: OnRenderHtmlAsync = async (
|
export const onRenderHtml: OnRenderHtmlAsync = async (
|
||||||
pageContext
|
pageContext
|
||||||
@ -15,9 +15,9 @@ export const onRenderHtml: OnRenderHtmlAsync = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const page = ReactDOMServer.renderToString(
|
const page = ReactDOMServer.renderToString(
|
||||||
<Layout pageContext={pageContext}>
|
<App pageContext={pageContext}>
|
||||||
<Page />
|
<Page />
|
||||||
</Layout>
|
</App>
|
||||||
);
|
);
|
||||||
|
|
||||||
// See https://vike.dev/head
|
// See https://vike.dev/head
|
||||||
|
@ -5,19 +5,23 @@ import Providers from "./providers";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import "nprogress/nprogress.css";
|
import "nprogress/nprogress.css";
|
||||||
|
|
||||||
type LayoutProps = {
|
type AppProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
pageContext: PageContext;
|
pageContext: PageContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Layout = ({ children, pageContext }: LayoutProps) => {
|
const App = ({ children, pageContext }: AppProps) => {
|
||||||
|
const { Layout } = pageContext.config;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<PageContextProvider pageContext={pageContext}>
|
<PageContextProvider pageContext={pageContext}>
|
||||||
<Providers>{children}</Providers>
|
<Providers>
|
||||||
|
{Layout ? <Layout children={children} /> : children}
|
||||||
|
</Providers>
|
||||||
</PageContextProvider>
|
</PageContextProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Layout;
|
export default App;
|
@ -3,7 +3,7 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-slate-900 text-white;
|
@apply bg-background text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-theme {
|
.cm-theme {
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
import { ComponentProps } from "react";
|
import React from "react";
|
||||||
import { usePageContext } from "./context";
|
import { usePageContext } from "./context";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
|
||||||
const Link = (props: ComponentProps<"a">) => {
|
type LinkProps = {
|
||||||
|
href?: string;
|
||||||
|
className?: string;
|
||||||
|
asChild?: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Link = ({ asChild, href, ...props }: LinkProps) => {
|
||||||
const pageContext = usePageContext();
|
const pageContext = usePageContext();
|
||||||
const { urlPathname } = pageContext;
|
const { urlPathname } = pageContext;
|
||||||
const { href } = props;
|
const Comp = asChild ? Slot : "a";
|
||||||
|
|
||||||
const isActive =
|
const isActive =
|
||||||
href === "/" ? urlPathname === href : urlPathname.startsWith(href!);
|
href === "/" ? urlPathname === href : urlPathname.startsWith(href!);
|
||||||
|
|
||||||
const className = [props.className, isActive && "is-active"]
|
return <Comp data-active={isActive || undefined} href={href} {...props} />;
|
||||||
.filter(Boolean)
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
return <a {...props} className={className} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Link;
|
export default Link;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { Request } from "express";
|
import type { Request, Response } from "express";
|
||||||
|
import type { UserSchema } from "~/server/db/schema/user";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Vike {
|
namespace Vike {
|
||||||
@ -11,10 +12,14 @@ declare global {
|
|||||||
config: {
|
config: {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
Layout?: (props: { children: React.ReactNode }) => React.ReactElement;
|
||||||
};
|
};
|
||||||
abortReason?: string;
|
abortReason?: string;
|
||||||
|
|
||||||
req: Request;
|
req: Request;
|
||||||
|
res: Response;
|
||||||
cookies: Record<string, string>;
|
cookies: Record<string, string>;
|
||||||
|
user?: UserSchema | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Request } from "express";
|
import { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||||
|
|
||||||
export const createContext = async ({ req }: { req: Request }) => {
|
export const createContext = async (ctx: CreateExpressContextOptions) => {
|
||||||
return {};
|
return ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Context = Awaited<ReturnType<typeof createContext>>;
|
export type Context = Awaited<ReturnType<typeof createContext>>;
|
||||||
|
@ -5,7 +5,7 @@ import { PageContext } from "vike/types";
|
|||||||
|
|
||||||
const trpcServer = async (ctx: PageContext) => {
|
const trpcServer = async (ctx: PageContext) => {
|
||||||
const createCaller = createCallerFactory(appRouter);
|
const createCaller = createCallerFactory(appRouter);
|
||||||
const context = await createContext({ req: ctx.req });
|
const context = await createContext(ctx);
|
||||||
return createCaller(context);
|
return createCaller(context);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,4 +17,5 @@ export const user = sqliteTable("users", {
|
|||||||
export const insertUserSchema = createInsertSchema(user);
|
export const insertUserSchema = createInsertSchema(user);
|
||||||
export const selectUserSchema = createSelectSchema(user);
|
export const selectUserSchema = createSelectSchema(user);
|
||||||
|
|
||||||
export type UserSchema = z.infer<typeof selectUserSchema>;
|
export type UserWithPassword = z.infer<typeof selectUserSchema>;
|
||||||
|
export type UserSchema = Omit<UserWithPassword, "password">;
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import "dotenv/config";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { renderPage } from "vike/server";
|
import { renderPage } from "vike/server";
|
||||||
import { IS_DEV } from "./lib/consts";
|
import { IS_DEV } from "./lib/consts";
|
||||||
import cookieParser from "cookie-parser";
|
import cookieParser from "cookie-parser";
|
||||||
import api from "./api";
|
import api from "./api";
|
||||||
|
import { authMiddleware } from "./middlewares/auth";
|
||||||
|
|
||||||
async function createServer() {
|
async function createServer() {
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -24,12 +26,13 @@ async function createServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
app.use(authMiddleware);
|
||||||
|
|
||||||
app.use("/api", api);
|
app.use("/api", api);
|
||||||
|
|
||||||
app.use("*", async (req, res, next) => {
|
app.use("*", async (req, res, next) => {
|
||||||
const url = req.originalUrl;
|
const url = req.originalUrl;
|
||||||
const pageContext = { req, cookies: req.cookies };
|
const pageContext = { req, res, cookies: req.cookies, user: req.user };
|
||||||
const ctx = await renderPage({ urlOriginal: url, ...pageContext });
|
const ctx = await renderPage({ urlOriginal: url, ...pageContext });
|
||||||
|
|
||||||
const { httpResponse } = ctx;
|
const { httpResponse } = ctx;
|
||||||
|
26
server/lib/jwt.ts
Normal file
26
server/lib/jwt.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
const JWT_KEY =
|
||||||
|
process.env.JWT_KEY || "e2b185bd471dda7a14cb8d9cfb8d1c568ac27926";
|
||||||
|
|
||||||
|
export type TokenData = {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createToken = (payload: TokenData) => {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
jwt.sign(payload, JWT_KEY, { expiresIn: "30d" }, function (err, token) {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(token as string);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const verifyToken = (token: string) => {
|
||||||
|
return new Promise<TokenData>((resolve, reject) => {
|
||||||
|
jwt.verify(token, JWT_KEY, function (err, decoded) {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(decoded as TokenData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
38
server/middlewares/auth.ts
Normal file
38
server/middlewares/auth.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import { verifyToken } from "../lib/jwt";
|
||||||
|
import db from "../db";
|
||||||
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
|
import { user } from "../db/schema/user";
|
||||||
|
|
||||||
|
export const authMiddleware = async (
|
||||||
|
req: Request,
|
||||||
|
_res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const token = req.cookies["auth-token"];
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("No token in cookies.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await verifyToken(token);
|
||||||
|
if (!data?.id) {
|
||||||
|
throw new Error("Invalid token!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await db.query.user.findFirst({
|
||||||
|
where: and(eq(user.id, data.id), isNull(user.deletedAt)),
|
||||||
|
columns: { password: false },
|
||||||
|
});
|
||||||
|
if (!userData) {
|
||||||
|
throw new Error("User is not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// store userdata to every request
|
||||||
|
(req as any).user = userData;
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
@ -1,8 +1,10 @@
|
|||||||
import { router } from "../api/trpc";
|
import { router } from "../api/trpc";
|
||||||
|
import auth from "./auth";
|
||||||
import project from "./project";
|
import project from "./project";
|
||||||
import file from "./file";
|
import file from "./file";
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
|
auth,
|
||||||
project,
|
project,
|
||||||
file,
|
file,
|
||||||
});
|
});
|
||||||
|
54
server/routers/auth.ts
Normal file
54
server/routers/auth.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { procedure, router } from "../api/trpc";
|
||||||
|
import db from "../db";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
|
import { user } from "../db/schema/user";
|
||||||
|
import { verifyPassword } from "../lib/crypto";
|
||||||
|
import { createToken } from "../lib/jwt";
|
||||||
|
|
||||||
|
const authRouter = router({
|
||||||
|
login: procedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(1),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const userData = await db.query.user.findFirst({
|
||||||
|
where: and(eq(user.email, input.email), isNull(user.deletedAt)),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userData) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Email is not found!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await verifyPassword(userData.password, input.password))) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Invalid password!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// set user token
|
||||||
|
const token = await createToken({ id: userData.id });
|
||||||
|
ctx.res.cookie("auth-token", token, { httpOnly: true });
|
||||||
|
|
||||||
|
return { ...userData, password: undefined };
|
||||||
|
}),
|
||||||
|
|
||||||
|
logout: procedure.mutation(({ ctx }) => {
|
||||||
|
ctx.res.cookie("auth-token", null, {
|
||||||
|
httpOnly: true,
|
||||||
|
expires: new Date(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default authRouter;
|
@ -9,6 +9,11 @@ import {
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { uid } from "../lib/utils";
|
import { uid } from "../lib/utils";
|
||||||
|
import { insertUserSchema, user } from "../db/schema/user";
|
||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { ucwords } from "~/lib/utils";
|
||||||
|
import { hashPassword } from "../lib/crypto";
|
||||||
|
import { createToken } from "../lib/jwt";
|
||||||
|
|
||||||
const projectRouter = router({
|
const projectRouter = router({
|
||||||
getAll: procedure.query(async () => {
|
getAll: procedure.query(async () => {
|
||||||
@ -45,19 +50,59 @@ const projectRouter = router({
|
|||||||
|
|
||||||
create: procedure
|
create: procedure
|
||||||
.input(
|
.input(
|
||||||
insertProjectSchema.pick({
|
insertProjectSchema
|
||||||
|
.pick({
|
||||||
title: true,
|
title: true,
|
||||||
})
|
})
|
||||||
|
.merge(
|
||||||
|
z.object({
|
||||||
|
forkFromId: z.number().optional(),
|
||||||
|
user: insertUserSchema
|
||||||
|
.pick({
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
password: true,
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const title =
|
||||||
|
input.title.length > 0 ? input.title : ucwords(faker.lorem.words(2));
|
||||||
|
let userId = 0;
|
||||||
|
|
||||||
|
return db.transaction(async (tx) => {
|
||||||
|
if (input.user) {
|
||||||
|
const [usr] = await tx
|
||||||
|
.insert(user)
|
||||||
|
.values({
|
||||||
|
...input.user,
|
||||||
|
password: await hashPassword(input.user.password),
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
userId = usr.id;
|
||||||
|
|
||||||
|
// set user token
|
||||||
|
const token = await createToken({ id: userId });
|
||||||
|
ctx.res.cookie("auth-token", token, { httpOnly: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("Invalid userId!");
|
||||||
|
}
|
||||||
|
|
||||||
const data: z.infer<typeof insertProjectSchema> = {
|
const data: z.infer<typeof insertProjectSchema> = {
|
||||||
userId: 1,
|
userId,
|
||||||
title: input.title,
|
title,
|
||||||
slug: uid(),
|
slug: uid(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const [result] = await db.insert(project).values(data).returning();
|
const [result] = await tx.insert(project).values(data).returning();
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: procedure
|
update: procedure
|
||||||
@ -95,6 +140,23 @@ const projectRouter = router({
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getTemplates: procedure.query(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: "Empty Project",
|
||||||
|
projectId: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Vanilla HTML+CSS+JS",
|
||||||
|
projectId: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "React + Tailwindcss",
|
||||||
|
projectId: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default projectRouter;
|
export default projectRouter;
|
||||||
|
11
server/types.ts
Normal file
11
server/types.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { UserSchema } from "./db/schema/user";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
user?: UserSchema | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
@ -17,6 +17,9 @@ const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
|
colors: {
|
||||||
|
background: "#050505",
|
||||||
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
"accordion-down": {
|
"accordion-down": {
|
||||||
from: { height: "0" },
|
from: { height: "0" },
|
||||||
|
Loading…
x
Reference in New Issue
Block a user