├── .dockerignore ├── public ├── favicon.ico └── images │ ├── screenshot1.png │ ├── screenshot2.png │ └── screenshot3.png ├── .eslintrc.json ├── lib ├── auth-client.ts ├── utils.ts ├── db.ts ├── supabaseClient.ts ├── server │ └── session.ts ├── auth.ts └── minio.ts ├── postcss.config.mjs ├── app ├── api │ ├── auth │ │ └── [...all] │ │ │ └── route.ts │ ├── comments │ │ ├── [id] │ │ │ └── route.ts │ │ └── route.ts │ ├── project │ │ └── [id] │ │ │ └── route.ts │ └── projects │ │ ├── route.ts │ │ └── [id] │ │ └── like │ │ └── route.ts ├── project-form │ ├── layout.tsx │ ├── page.tsx │ └── _components │ │ └── Form.tsx ├── project │ └── [id] │ │ ├── layout.tsx │ │ └── page.tsx ├── edit-form │ └── [id] │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── _components │ │ └── Edit.tsx ├── auth │ ├── layout.tsx │ ├── sign-in │ │ └── page.tsx │ └── sign-up │ │ └── page.tsx ├── (ProviderWrapper) │ └── layout.tsx ├── page.tsx ├── projects │ ├── layout.tsx │ └── page.tsx ├── layout.tsx └── globals.css ├── drizzle.config.ts ├── drizzle ├── meta │ ├── _journal.json │ └── 0000_snapshot.json └── 0000_great_squadron_supreme.sql ├── components ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── input.tsx │ ├── sonner.tsx │ ├── spinner.tsx │ ├── popover.tsx │ ├── avatar.tsx │ ├── Spotlight.tsx │ ├── alert.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── form.tsx │ └── dropdown-menu.tsx ├── container.tsx ├── nothing-here.tsx ├── footer.tsx ├── project-filter.tsx ├── hero.tsx ├── BulbLoading.tsx ├── header.tsx ├── icons │ └── icons.tsx ├── project-card.tsx ├── comments.tsx ├── projects-container.tsx └── github-stats.tsx ├── next.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── better-auth_migrations └── 2024-10-17T12-14-00.359Z.sql ├── Dockerfile ├── schema └── index.ts ├── types └── index.ts ├── LICENSE ├── db ├── relations.ts └── schema.ts ├── README.md ├── constants └── index.ts ├── package.json └── tailwind.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manuel-heav/built/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manuel-heav/built/HEAD/public/images/screenshot1.png -------------------------------------------------------------------------------- /public/images/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manuel-heav/built/HEAD/public/images/screenshot2.png -------------------------------------------------------------------------------- /public/images/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manuel-heav/built/HEAD/public/images/screenshot3.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-empty-interface": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/auth-client.ts: -------------------------------------------------------------------------------- 1 | import { createAuthClient } from "better-auth/react"; 2 | export const authClient = createAuthClient({ 3 | baseURL: process.env.NEXT_PUBLIC_BASE_URL, 4 | }); 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/lib/auth"; 2 | import { toNextJsHandler } from "better-auth/next-js"; 3 | 4 | export const { POST, GET } = toNextJsHandler(auth); 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/db.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from "pg"; 2 | import { drizzle } from "drizzle-orm/node-postgres"; 3 | 4 | const pool = new Pool({ 5 | connectionString: process.env.DATABASE_URL, 6 | ssl: false, 7 | }); 8 | 9 | export const db = drizzle(pool); 10 | 11 | 12 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | 3 | export default defineConfig({ 4 | schema: "./db/schema.ts", 5 | out: "./drizzle", 6 | dialect: "postgresql", 7 | dbCredentials: { 8 | url: process.env.DATABASE_URL!, 9 | }, 10 | }); -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1755369915608, 9 | "tag": "0000_great_squadron_supreme", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /lib/supabaseClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | 3 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL as string; 4 | const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_KEY as string; 5 | 6 | export const supabase = createClient(supabaseUrl, supabaseKey); 7 | -------------------------------------------------------------------------------- /app/project-form/layout.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/header"; 2 | 3 | export default function RootLayout({ 4 | children, 5 | }: Readonly<{ 6 | children: React.ReactNode; 7 | }>) { 8 | return ( 9 |
10 |
11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/project/[id]/layout.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/header"; 2 | 3 | export default function RootLayout({ 4 | children, 5 | }: Readonly<{ 6 | children: React.ReactNode; 7 | }>) { 8 | return ( 9 |
10 |
11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/edit-form/[id]/layout.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/header"; 2 | 3 | export default function RootLayout({ 4 | children, 5 | }: Readonly<{ 6 | children: React.ReactNode; 7 | }>) { 8 | return ( 9 |
10 |
11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /app/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "@/components/footer"; 2 | import Header from "@/components/header"; 3 | 4 | export default function RootLayout({ 5 | children, 6 | }: Readonly<{ 7 | children: React.ReactNode; 8 | }>) { 9 | return ( 10 |
11 |
12 | {children} 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /components/container.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface ContainerProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | const Container: React.FC = ({ children }) => { 8 | return ( 9 |
10 |
{children}
11 |
12 | ); 13 | }; 14 | 15 | export default Container; 16 | -------------------------------------------------------------------------------- /app/(ProviderWrapper)/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 | 4 | const queryClient = new QueryClient(); 5 | 6 | export default function ProviderWrapperLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 | {children} 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const isCI = Boolean(process.env.CI); 3 | const nextConfig = { 4 | images: { 5 | domains: ['miftztgfemnglhjovkgg.supabase.co','salonlfc.com','minio.theblogrammer.com'], 6 | }, 7 | // Avoid Windows symlink issues locally; still use standalone in CI 8 | ...(isCI ? { output: 'standalone' } : {}), 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /lib/server/session.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { auth } from "@/lib/auth"; 4 | import { headers } from "next/headers"; 5 | 6 | export async function getSession() { 7 | try { 8 | const session = await auth.api.getSession({ 9 | headers: await headers() 10 | }) 11 | return session 12 | } catch (error) { 13 | console.error(error) 14 | return null 15 | } 16 | } -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect } from "react"; 3 | import { useRouter } from "next/navigation"; 4 | import { authClient } from "@/lib/auth-client"; 5 | 6 | const Home = () => { 7 | const router = useRouter(); 8 | const { isPending } = authClient.useSession(); 9 | 10 | useEffect(() => { 11 | if (!isPending) { 12 | router.push("/projects"); 13 | } 14 | }, [isPending, router]); 15 | 16 | return null; 17 | }; 18 | 19 | export default Home; 20 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/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 | } -------------------------------------------------------------------------------- /app/projects/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 | 4 | const queryClient = new QueryClient({ 5 | defaultOptions: { 6 | queries: { 7 | staleTime: 1000 * 60 * 5, 8 | refetchOnWindowFocus: false, 9 | }, 10 | }, 11 | }); 12 | 13 | export default function ProviderWrapperLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | {children} 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts -------------------------------------------------------------------------------- /components/nothing-here.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Ghost } from 'lucide-react' 3 | 4 | const NothingHere = () => { 5 | return ( 6 |
7 | 8 |

Oops! It's a ghost town here!

9 |

10 | Be the first person to post under this tag. 11 |

12 |
13 | ) 14 | } 15 | 16 | export default NothingHere -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | 4 | const Footer = () => { 5 | return ( 6 |
7 |

8 | Made with 💖 by{" "} 9 | 10 | 11 | The Blogrammer 12 | 13 | 14 |

15 |
16 | ); 17 | }; 18 | 19 | export default Footer; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { betterAuth } from "better-auth"; 2 | import { Pool } from "pg"; 3 | 4 | const pool = new Pool({ 5 | connectionString: process.env.DATABASE_URL, 6 | ssl: false 7 | }); 8 | 9 | export const auth = betterAuth({ 10 | database: pool, 11 | url: process.env.BETTER_AUTH_URL, 12 | emailAndPassword: { 13 | enabled: true, 14 | }, 15 | socialProviders: { 16 | github: { 17 | clientId: process.env.GITHUB_CLIENT_ID as string, 18 | clientSecret: process.env.GITHUB_CLIENT_SECRET as string, 19 | }, 20 | google: { 21 | clientId: process.env.GOOGLE_CLIENT_ID as string, 22 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /app/projects/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import BulbLoading from "@/components/BulbLoading"; 3 | import Footer from "@/components/footer"; 4 | import Header from "@/components/header"; 5 | import Hero from "@/components/hero"; 6 | import ProjectsContainer from "@/components/projects-container"; 7 | import { authClient } from "@/lib/auth-client"; 8 | import React from "react"; 9 | 10 | const Home = () => { 11 | const { data: session, isPending } = authClient.useSession(); 12 | 13 | if (isPending) { 14 | return ; 15 | } 16 | 17 | return ( 18 |
19 |
20 | {!session && } 21 | 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Home; 28 | -------------------------------------------------------------------------------- /better-auth_migrations/2024-10-17T12-14-00.359Z.sql: -------------------------------------------------------------------------------- 1 | create table "user" ("id" text primary key, "name" text not null, "email" text not null unique, "emailVerified" boolean not null, "image" text, "createdAt" date not null, "updatedAt" date not null); 2 | 3 | create table "session" ("id" text primary key, "expiresAt" date not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id")); 4 | 5 | create table "account" ("id" text primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id"), "accessToken" text, "refreshToken" text, "idToken" text, "expiresAt" date, "password" text); 6 | 7 | create table "verification" ("id" text primary key, "identifier" text not null, "value" text not null, "expiresAt" date not null) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 1. Dependencies 2 | FROM node:20-alpine AS deps 3 | WORKDIR /app 4 | RUN corepack enable 5 | COPY package.json pnpm-lock.yaml* ./ 6 | RUN pnpm install --frozen-lockfile 7 | 8 | # 2. Build 9 | FROM node:20-alpine AS builder 10 | WORKDIR /app 11 | RUN corepack enable 12 | COPY . . 13 | COPY --from=deps /app/node_modules ./node_modules 14 | COPY --from=deps /app/pnpm-lock.yaml ./pnpm-lock.yaml 15 | RUN pnpm build 16 | 17 | # 3. Runtime 18 | FROM node:20-alpine AS runner 19 | WORKDIR /app 20 | RUN corepack enable 21 | ENV NODE_ENV=production 22 | ENV NEXT_TELEMETRY_DISABLED=1 23 | 24 | COPY --from=builder /app/next.config.mjs ./ 25 | COPY --from=builder /app/public ./public 26 | COPY --from=builder /app/.next/standalone ./ 27 | COPY --from=builder /app/.next/static ./.next/static 28 | 29 | EXPOSE 3000 30 | CMD ["node", "server.js"] 31 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /schema/index.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const formSchema = z.object({ 4 | title: z 5 | .string() 6 | .min(2, "Title must be at least 2 characters") 7 | .max(100, "Title must be less than 100 characters"), 8 | description: z 9 | .string() 10 | .min(10, "Description must be at least 10 characters") 11 | .max(500, "Description must be less than 500 characters"), 12 | imageUrl: z.string().url("Please provide a valid URL for the image"), 13 | tags: z 14 | .array(z.string()) 15 | .min(1, "Please select at least one tag") 16 | .max(3, "You can select up to 3 tags"), 17 | githubRepo: z.string().url("Please provide a valid GitHub repository URL"), 18 | liveDemo: z.string().url("Please provide a valid live demo URL"), 19 | telegramChannel: z.string().optional(), 20 | documentation: z.string().optional(), 21 | }); 22 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | /* eslint-disable @typescript-eslint/no-empty-interface */ 6 | 7 | export type TextareaProps = React.TextareaHTMLAttributes; 8 | 9 | const Textarea = React.forwardRef( 10 | ({ className, ...props }, ref) => { 11 | return ( 12 |