├── README.md ├── src ├── pages │ ├── login.tsx │ ├── register.tsx │ ├── profile.tsx │ ├── api │ │ └── trpc │ │ │ └── [trpc].ts │ ├── _app.tsx │ └── index.tsx ├── lib │ ├── supabase │ │ ├── bucket.ts │ │ ├── client.ts │ │ ├── server.ts │ │ └── authErrorCodes.ts │ └── utils.ts ├── features │ ├── auth │ │ ├── forms │ │ │ └── register.ts │ │ ├── components │ │ │ └── RegisterFormInner.tsx │ │ └── pages │ │ │ ├── RegisterPage.tsx │ │ │ └── LoginPage.tsx │ └── profile │ │ ├── forms │ │ └── edit-profile.ts │ │ ├── components │ │ └── EditProfileFormInner.tsx │ │ └── pages │ │ └── ProfilePage.tsx ├── components │ ├── theme-provider.tsx │ ├── layout │ │ ├── Header.tsx │ │ ├── AuthRoute.tsx │ │ ├── GuestRoute.tsx │ │ ├── SectionContainer.tsx │ │ ├── PageContainer.tsx │ │ └── HeadMetaData.tsx │ └── ui │ │ ├── textarea.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── checkbox.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ └── form.tsx ├── schemas │ └── auth.ts ├── server │ ├── db.ts │ └── api │ │ ├── root.ts │ │ ├── routers │ │ ├── auth.ts │ │ └── profile.ts │ │ └── trpc.ts ├── env.js ├── styles │ └── globals.css └── utils │ └── api.ts ├── postcss.config.js ├── public └── favicon.ico ├── prettier.config.js ├── components.json ├── .env.example ├── next.config.js ├── prisma └── schema.prisma ├── .gitignore ├── tsconfig.json ├── .eslintrc.cjs ├── tailwind.config.ts ├── start-database.sh └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # Qepo Lu Q\*nt\*l -------------------------------------------------------------------------------- /src/pages/login.tsx: -------------------------------------------------------------------------------- 1 | export { default } from "~/features/auth/pages/LoginPage"; -------------------------------------------------------------------------------- /src/pages/register.tsx: -------------------------------------------------------------------------------- 1 | export { default } from '~/features/auth/pages/RegisterPage' -------------------------------------------------------------------------------- /src/pages/profile.tsx: -------------------------------------------------------------------------------- 1 | export { default } from "~/features/profile/pages/ProfilePage"; -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodevoid/qepo-nextjs/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/lib/supabase/bucket.ts: -------------------------------------------------------------------------------- 1 | export enum SUPABASE_BUCKET { 2 | ProfilePictures = "profile-pictures" 3 | } -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ 2 | export default { 3 | plugins: ["prettier-plugin-tailwindcss"], 4 | }; 5 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/features/auth/forms/register.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { emailSchema, passwordSchema } from "~/schemas/auth"; 3 | 4 | export const registerFormSchema = z.object({ 5 | email: emailSchema, 6 | password: passwordSchema, 7 | }); 8 | 9 | export type RegisterFormSchema = z.infer; 10 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ThemeProvider as NextThemesProvider } from "next-themes" 3 | 4 | export function ThemeProvider({ 5 | children, 6 | ...props 7 | }: React.ComponentProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /src/features/profile/forms/edit-profile.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const editProfileFormSchema = z.object({ 4 | username: z 5 | .string() 6 | .min(3, { message: "Username minimal 3 karakter" }) 7 | .max(16, { message: "Username maksimal 16 karakter" }), 8 | bio: z.string().optional(), 9 | }); 10 | 11 | export type EditProfileFormSchema = z.infer; 12 | -------------------------------------------------------------------------------- /src/components/layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export const Header = () => { 4 | return ( 5 |
6 | 10 | Qepo 11 | 12 |
13 | ); 14 | }; -------------------------------------------------------------------------------- /src/schemas/auth.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const passwordSchema = z 4 | .string({ message: "Password wajib diisi" }) 5 | .min(8, { message: "Password minimal 8 karakter" }) 6 | .regex(/[a-z]/,{ message: "Password minimal 1 huruf kecil"}) 7 | .regex(/[A-Z]/, { message: "Password minimal 1 huruf besar"}) 8 | .regex(/[0-9]/, { message: "Password minimal 1 angka"}); 9 | 10 | export const emailSchema = z 11 | .string({ message: "Email wajib diisi" }) 12 | .email({ message: "Format email tidak tepat" }); 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "~/components", 15 | "utils": "~/lib/utils", 16 | "ui": "~/components/ui", 17 | "lib": "~/lib", 18 | "hooks": "~/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /src/server/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | import { env } from "~/env"; 4 | 5 | const createPrismaClient = () => 6 | new PrismaClient({ 7 | log: 8 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], 9 | }); 10 | 11 | const globalForPrisma = globalThis as unknown as { 12 | prisma: ReturnType | undefined; 13 | }; 14 | 15 | export const db = globalForPrisma.prisma ?? createPrismaClient(); 16 | 17 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; 18 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | 2 | # Connect to Supabase via connection pooling with Supavisor. 3 | DATABASE_URL="postgresql://postgres.pzdidkaxnpkfrnpvfaeb:[YOUR-PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true" 4 | 5 | # Direct connection to the database. Used for migrations. 6 | DIRECT_URL="postgresql://postgres.pzdidkaxnpkfrnpvfaeb:[YOUR-PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:5432/postgres" 7 | 8 | NEXT_PUBLIC_BASE_URL="http://localhost:3000" 9 | 10 | # Supabase 11 | NEXT_PUBLIC_SUPABASE_URL="" 12 | NEXT_PUBLIC_SUPABASE_ANON_KEY="" 13 | SUPABASE_SERVICE_ROLE_KEY="" -------------------------------------------------------------------------------- /src/lib/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createBrowserClient 3 | } from "@supabase/ssr"; 4 | import { createClient as createDefaultClient } from "@supabase/supabase-js"; 5 | 6 | function createClient() { 7 | const supabase = createBrowserClient( 8 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 9 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 10 | ); 11 | 12 | return supabase; 13 | } 14 | 15 | export const supabaseDefaultClient = createDefaultClient( 16 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 17 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 18 | ); 19 | 20 | export const supabase = createClient(); 21 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | import "./src/env.js"; 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: true, 10 | 11 | /** 12 | * If you are using `appDir` then you must comment the below `i18n` config out. 13 | * 14 | * @see https://github.com/vercel/next.js/issues/41980 15 | */ 16 | i18n: { 17 | locales: ["en"], 18 | defaultLocale: "en", 19 | }, 20 | transpilePackages: ["geist"], 21 | }; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /src/components/layout/AuthRoute.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { type PropsWithChildren, useEffect } from "react"; 3 | import { supabase } from "~/lib/supabase/client"; 4 | 5 | export const AuthRoute = (props: PropsWithChildren) => { 6 | const router = useRouter(); 7 | 8 | useEffect(() => { 9 | void (async function () { 10 | const { data } = await supabase.auth.getUser(); 11 | 12 | if (!data.user) { 13 | await router.replace("/"); 14 | } 15 | })(); 16 | // eslint-disable-next-line react-hooks/exhaustive-deps 17 | }, []); 18 | 19 | return props.children; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/layout/GuestRoute.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { type PropsWithChildren, useEffect } from "react"; 3 | import { supabase } from "~/lib/supabase/client"; 4 | 5 | export const GuestRoute = (props: PropsWithChildren) => { 6 | const router = useRouter(); 7 | 8 | useEffect(() => { 9 | void (async function () { 10 | const { data } = await supabase.auth.getUser(); 11 | 12 | if (data.user) { 13 | await router.replace("/"); 14 | } 15 | })(); 16 | // eslint-disable-next-line react-hooks/exhaustive-deps 17 | }, []); 18 | 19 | return props.children; 20 | }; 21 | -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { createNextApiHandler } from "@trpc/server/adapters/next"; 2 | 3 | import { env } from "~/env"; 4 | import { appRouter } from "~/server/api/root"; 5 | import { createTRPCContext } from "~/server/api/trpc"; 6 | 7 | // export API handler 8 | export default createNextApiHandler({ 9 | router: appRouter, 10 | createContext: createTRPCContext, 11 | onError: 12 | env.NODE_ENV === "development" 13 | ? ({ path, error }) => { 14 | console.error( 15 | `❌ tRPC failed on ${path ?? ""}: ${error.message}` 16 | ); 17 | } 18 | : undefined, 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "~/lib/utils" 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<"textarea"> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |