mirror of
				https://github.com/khairul169/code-share.git
				synced 2025-11-04 05:31:08 +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