├── .github ├── CODEOWNERS ├── README.en.md ├── README.md ├── README.tr.md ├── dependabot.yml ├── noob.gg-PRD.md └── workflows │ └── congratulations.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── apps ├── api │ ├── .gitignore │ ├── README.md │ ├── README_VERSIONING.md │ ├── drizzle.config.ts │ ├── drizzle │ │ ├── 0000_round_payback.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ └── _journal.json │ ├── eslint.config.mjs │ ├── openapi.yaml │ ├── package.json │ ├── src │ │ ├── controllers │ │ │ ├── distributors.controller.test.ts │ │ │ ├── event-attendees.controller.test.ts │ │ │ └── v1 │ │ │ │ ├── any-route.controller.ts │ │ │ │ ├── distributors.controller.ts │ │ │ │ ├── event-attendees.controller.ts │ │ │ │ ├── event-invitations.controller.ts │ │ │ │ ├── game-modes.controller.ts │ │ │ │ ├── game-platforms.controller.ts │ │ │ │ ├── game-ranks.controller.ts │ │ │ │ ├── game-regions.controller.ts │ │ │ │ ├── games.controller.ts │ │ │ │ ├── languages.controller.ts │ │ │ │ ├── lobbies.controller.ts │ │ │ │ ├── main.controller.ts │ │ │ │ ├── platforms.controller.ts │ │ │ │ └── user-profiles.controller.ts │ │ ├── db │ │ │ ├── index.ts │ │ │ └── schemas │ │ │ │ ├── distributors.drizzle.ts │ │ │ │ ├── event-attendees.drizzle.ts │ │ │ │ ├── event-invitations.drizzle.ts │ │ │ │ ├── events.drizzle.ts │ │ │ │ ├── game-distributors.drizzle.ts │ │ │ │ ├── game-modes.drizzle.ts │ │ │ │ ├── game-platforms.drizzle.ts │ │ │ │ ├── game-ranks.drizzle.ts │ │ │ │ ├── games.drizzle.ts │ │ │ │ ├── languages.drizzle.ts │ │ │ │ ├── lobbies.drizzle.ts │ │ │ │ ├── lobby-languages.drizzle.ts │ │ │ │ ├── lobby-members.drizzle.ts │ │ │ │ ├── platforms.drizzle.ts │ │ │ │ ├── schema.ts │ │ │ │ └── user-profile.drizzle.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── zod-schemas │ │ │ │ └── game-ranks.ts │ │ ├── middleware │ │ │ ├── deprecation.ts │ │ │ └── version.ts │ │ ├── routes │ │ │ ├── index.ts │ │ │ └── v1 │ │ │ │ ├── distributors.ts │ │ │ │ ├── event-attendees.ts │ │ │ │ ├── event-invitations.ts │ │ │ │ ├── game-ranks.ts │ │ │ │ ├── games.ts │ │ │ │ ├── index.ts │ │ │ │ ├── platforms.ts │ │ │ │ └── user-profiles.ts │ │ ├── types │ │ │ └── yamljs.d.ts │ │ └── utils │ │ │ └── bigint-serializer.ts │ └── tsconfig.json └── web │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── app │ ├── (root) │ │ ├── layout.tsx │ │ └── page.tsx │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── dashboard │ │ ├── gameranks │ │ │ ├── [id] │ │ │ │ └── edit │ │ │ │ │ └── page.tsx │ │ │ ├── new │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── games │ │ │ ├── [id] │ │ │ │ └── edit │ │ │ │ │ └── page.tsx │ │ │ ├── new │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── platforms │ │ │ ├── [id] │ │ │ └── edit │ │ │ │ └── page.tsx │ │ │ ├── new │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── layout.tsx │ ├── profile-demo │ │ └── page.tsx │ └── profile │ │ └── [username] │ │ └── page.tsx │ ├── auth.ts │ ├── components.json │ ├── components │ ├── LoginButton.tsx │ ├── LogoutButton.tsx │ ├── dashboard │ │ └── layout │ │ │ ├── header │ │ │ └── index.tsx │ │ │ └── sidebar │ │ │ ├── index.tsx │ │ │ ├── nav-main.tsx │ │ │ ├── nav-user.tsx │ │ │ └── team-switcher.tsx │ ├── gameranks │ │ ├── gamerank-form.tsx │ │ └── gamerank-table.tsx │ ├── games │ │ ├── game-card.tsx │ │ ├── game-form.tsx │ │ └── game-table.tsx │ ├── language-switcher.tsx │ ├── mvpblocks │ │ ├── landing-header.tsx │ │ ├── landing-hero-section.tsx │ │ └── sparkles-logo.tsx │ ├── platforms │ │ ├── platform-form.tsx │ │ └── platform-table.tsx │ ├── query-provider.tsx │ ├── syntax-ui │ │ └── starwars-button.tsx │ ├── theme-switcher.tsx │ ├── ui │ │ ├── avatar.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── collapsible.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── navbar.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── sparkles.tsx │ │ ├── tabs.tsx │ │ └── tooltip.tsx │ └── user-profile │ │ ├── profile-header.tsx │ │ ├── profile-navigation.tsx │ │ ├── tabs │ │ ├── about-tab.tsx │ │ ├── friends-tab.tsx │ │ ├── gamer-experience-tab.tsx │ │ ├── groups-tab.tsx │ │ ├── photos-tab.tsx │ │ ├── professional-tab.tsx │ │ ├── reviews-tab.tsx │ │ └── timeline-tab.tsx │ │ └── user-profile-page.tsx │ ├── eslint.config.mjs │ ├── features │ ├── gameranks │ │ └── api │ │ │ ├── actions.ts │ │ │ └── use-gameranks.ts │ ├── games │ │ └── api │ │ │ ├── actions.ts │ │ │ └── use-games.ts │ ├── platforms │ │ └── api │ │ │ ├── actions.ts │ │ │ └── use-platforms.ts │ └── user-profiles │ │ └── api │ │ ├── actions.ts │ │ └── use-user-profiles.ts │ ├── hooks │ └── use-mobile.ts │ ├── i18n │ └── request.ts │ ├── lib │ └── utils.ts │ ├── messages │ ├── en.json │ └── tr.json │ ├── middleware.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── file.svg │ ├── flags │ │ ├── en.svg │ │ └── tr.svg │ ├── globe.svg │ ├── logos │ │ ├── counter-strike-2.svg │ │ ├── fortnite-logo.svg │ │ ├── league-of-legends-logo.svg │ │ ├── pubg-logo.webp │ │ ├── valorant-logo.png │ │ └── valorant-logo.svg │ ├── next.svg │ ├── noobgg-logo.png │ ├── site.webmanifest │ ├── vercel.svg │ └── window.svg │ ├── styles │ └── globals.css │ ├── tsconfig.json │ └── types │ ├── game.ts │ ├── gamerank.ts │ ├── platform.ts │ └── user-profile.ts ├── bun.lock ├── docker.md ├── docs ├── Profile UI Corrections.png ├── Screenshot 2025-06-02 204809.png ├── Vikinger Profile.png ├── globe.md ├── noobgg-logo.png └── search-lobby-mock-up-filled.png ├── package.json ├── packages ├── eslint-config │ ├── README.md │ ├── base.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── shared │ ├── .gitignore │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── schemas │ │ ├── distributor.schema.ts │ │ ├── event-attendees.ts │ │ ├── event-invitations.ts │ │ ├── example-schema.ts │ │ ├── game.schema.ts │ │ ├── gamerank.schema.ts │ │ ├── openapi-responses.schema.ts │ │ ├── platform.schema.ts │ │ └── user-profile.schema.ts │ └── tsconfig.json ├── typescript-config │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── eslint.config.mjs │ ├── package.json │ ├── src │ ├── button.tsx │ ├── card.tsx │ └── code.tsx │ ├── tsconfig.json │ └── turbo │ └── generators │ ├── config.ts │ └── templates │ └── component.hbs └── turbo.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @altudev 2 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # noob.gg 🎮 2 | 3 |
4 | noob.gg logo 5 | 6 | [![Turborepo](https://img.shields.io/badge/Turborepo-EF4444?style=for-the-badge&logo=turborepo&logoColor=white)](https://turbo.build/repo) 7 | [![Next.js](https://img.shields.io/badge/Next.js-000000?style=for-the-badge&logo=next.js&logoColor=white)](https://nextjs.org/) 8 | [![Hono.js](https://img.shields.io/badge/Hono.js-00A3FF?style=for-the-badge&logo=hono&logoColor=white)](https://hono.dev/) 9 | [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) 10 | [![Bun](https://img.shields.io/badge/Bun-000000?style=for-the-badge&logo=bun&logoColor=white)](https://bun.sh/) 11 |
12 | 13 | ## 📚 Documentation 14 | 15 | The primary documentation for this project is in Turkish. An English version is also available. 16 | 17 | - **🇹🇷 Turkish Documentation (Primary):** [README.tr.md](./README.tr.md) 18 | - **🇬🇧 English Documentation (Secondary):** [README.en.md](./README.en.md) 19 | 20 | Please refer to these files for information on project setup, development workflow, technologies used, and contribution guidelines. 21 | 22 | --- 23 | 24 | ## 👥 Contributors 25 | 26 | Meet our amazing contributors: 27 | 28 |
29 | 30 | altudev 31 | 32 | 33 | Furkan Özay 34 | 35 | 36 | Hikmet Melik 37 | 38 | 39 | Ufuk Gürgen 40 | 41 | 42 | Ravi DULUNDU 43 | 44 | 45 | Ufuk Özen 46 | 47 | 48 | Taiizor 49 | 50 | 51 | Ahmet 52 | 53 | 54 | Abhishek85805 55 | 56 | 57 | Ahmet Can ÜZÜMCÜ 58 | 59 | 60 | Efe 61 | 62 | 63 | Jules (Google Labs AI) 64 | 65 | 66 | DevinAI Integration 67 | 68 |
69 | 70 |
71 | Built with ❤️ by the noob.gg team 72 |
73 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Updates for npm packages 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "monday" 9 | time: "09:00" 10 | timezone: "Europe/Istanbul" 11 | open-pull-requests-limit: 10 12 | versioning-strategy: "auto" 13 | labels: 14 | - "dependencies" 15 | - "npm" 16 | commit-message: 17 | prefix: "npm" 18 | include: "scope" 19 | pull-request-branch-name: 20 | separator: "-" 21 | groups: 22 | react-packages: 23 | patterns: 24 | - "react*" 25 | - "next*" 26 | update-types: 27 | - "minor" 28 | - "patch" 29 | dev-dependencies: 30 | dependency-type: "development" 31 | update-types: 32 | - "minor" 33 | - "patch" 34 | ignore: 35 | - dependency-name: "typescript" 36 | update-types: ["version-update:semver-major"] 37 | 38 | # Docker updates 39 | - package-ecosystem: "docker" 40 | directory: "/" 41 | schedule: 42 | interval: "weekly" 43 | day: "wednesday" 44 | time: "09:00" 45 | timezone: "Europe/Istanbul" 46 | open-pull-requests-limit: 5 47 | labels: 48 | - "dependencies" 49 | - "docker" 50 | commit-message: 51 | prefix: "docker" 52 | 53 | # GitHub Actions updates 54 | - package-ecosystem: "github-actions" 55 | directory: "/" 56 | schedule: 57 | interval: "weekly" 58 | day: "thursday" 59 | time: "09:00" 60 | timezone: "Europe/Istanbul" 61 | open-pull-requests-limit: 5 62 | labels: 63 | - "dependencies" 64 | - "github-actions" 65 | commit-message: 66 | prefix: "github-actions" 67 | -------------------------------------------------------------------------------- /.github/workflows/congratulations.yml: -------------------------------------------------------------------------------- 1 | name: Congratulations 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | issues: 7 | types: [opened] 8 | 9 | jobs: 10 | Congratulation: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | issues: write 16 | pull-requests: write 17 | 18 | steps: 19 | - uses: actions/first-interaction@v1 20 | with: 21 | repo-token: ${{ secrets.GITHUB_TOKEN }} 22 | issue-message: "We're grateful you made your first issue notification. You can be sure that a response will be made as soon as possible." 23 | pr-message: "We're grateful you made your first pull request notification. You can be sure that a response will be made as soon as possible." -------------------------------------------------------------------------------- /.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 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/.npmrc -------------------------------------------------------------------------------- /apps/api/.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /apps/api/README.md: -------------------------------------------------------------------------------- 1 | To install dependencies: 2 | ```sh 3 | bun install 4 | ``` 5 | 6 | To run: 7 | ```sh 8 | bun run dev 9 | ``` 10 | 11 | open http://localhost:3000 12 | -------------------------------------------------------------------------------- /apps/api/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { defineConfig } from 'drizzle-kit'; 3 | 4 | export default defineConfig({ 5 | out: './drizzle', 6 | schema: './src/db/schemas/schema.ts', 7 | dialect: 'postgresql', 8 | dbCredentials: { 9 | url: process.env.DATABASE_URL!, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /apps/api/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1749577911536, 9 | "tag": "0000_round_payback", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /apps/api/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { config } from "@repo/eslint-config/base"; 2 | 3 | export default [...config]; 4 | -------------------------------------------------------------------------------- /apps/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "scripts": { 4 | "dev": "bun run --hot src/index.ts", 5 | "lint": "eslint . --ext .ts", 6 | "db:generate": "drizzle-kit generate", 7 | "db:migrate": "drizzle-kit migrate", 8 | "db:studio": "drizzle-kit studio", 9 | "db:reset": "drizzle-kit drop && drizzle-kit generate && drizzle-kit migrate" 10 | }, 11 | "dependencies": { 12 | "@aws-sdk/client-s3": "^3.821.0", 13 | "dotenv": "^16.5.0", 14 | "drizzle-orm": "^0.44.1", 15 | "hono": "^4.7.11", 16 | "pg": "^8.16.0", 17 | "@repo/shared": "workspace:*", 18 | "zod": "^3.23.8" 19 | }, 20 | "devDependencies": { 21 | "@types/bun": "latest", 22 | "@types/pg": "^8.15.4", 23 | "drizzle-kit": "^0.31.1", 24 | "tsx": "^4.19.4", 25 | "vitest": "^3.2.3", 26 | "@repo/eslint-config": "*", 27 | "eslint": "^9.28.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/api/src/controllers/v1/any-route.controller.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | 3 | // v1: any-route controller (boş şablon) 4 | export const anyRouteGetController = (c: Context) => { 5 | return c.text("Hello Any Route!"); 6 | }; 7 | 8 | export const anyRoutePostController = (c: Context) => { 9 | return c.text("Hello Any Route!"); 10 | }; 11 | -------------------------------------------------------------------------------- /apps/api/src/controllers/v1/distributors.controller.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | import { eq } from "drizzle-orm"; 3 | import { db } from "../../db"; 4 | import { distributorsTable } from "../../db/schemas/distributors.drizzle"; 5 | import { 6 | createDistributorSchema, 7 | updateDistributorSchema, 8 | } from "@repo/shared"; 9 | 10 | export const getAllDistributorsController = async (c: Context) => { 11 | try { 12 | const distributors = await db.select().from(distributorsTable); 13 | return c.json(distributors); 14 | } catch (error) { 15 | return c.json({ error: "Internal server error" }, 500); 16 | } 17 | }; 18 | 19 | export const getDistributorByIdController = async (c: Context) => { 20 | try { 21 | const id = Number(c.req.param("id")); 22 | if (!Number.isInteger(id) || id <= 0) { 23 | return c.json({ error: "Invalid id" }, 400); 24 | } 25 | const distributor = await db 26 | .select() 27 | .from(distributorsTable) 28 | .where(eq(distributorsTable.id, id)); 29 | if (distributor.length === 0) 30 | return c.json({ error: "Distributor not found" }, 404); 31 | return c.json(distributor[0]); 32 | } catch (error) { 33 | return c.json({ error: "Internal server error" }, 500); 34 | } 35 | }; 36 | 37 | export const createDistributorController = async (c: Context) => { 38 | try { 39 | const data = await c.req.json(); 40 | const result = createDistributorSchema.safeParse(data); 41 | if (!result.success) { 42 | return c.json({ error: result.error.flatten().fieldErrors }, 400); 43 | } 44 | const [distributor] = await db 45 | .insert(distributorsTable) 46 | .values(result.data) 47 | .returning(); 48 | return c.json(distributor, 201); 49 | } catch (error) { 50 | return c.json({ error: "Internal server error" }, 500); 51 | } 52 | }; 53 | 54 | export const updateDistributorController = async (c: Context) => { 55 | try { 56 | const id = Number(c.req.param("id")); 57 | if (!Number.isInteger(id) || id <= 0) { 58 | return c.json({ error: "Invalid id" }, 400); 59 | } 60 | const data = await c.req.json(); 61 | const result = updateDistributorSchema.safeParse(data); 62 | if (!result.success) { 63 | return c.json({ error: result.error.flatten().fieldErrors }, 400); 64 | } 65 | if (Object.keys(result.data).length === 0) { 66 | return c.json({ error: "No valid fields provided for update" }, 400); 67 | } 68 | const [distributor] = await db 69 | .update(distributorsTable) 70 | .set(result.data) 71 | .where(eq(distributorsTable.id, id)) 72 | .returning(); 73 | if (!distributor) return c.json({ error: "Distributor not found" }, 404); 74 | return c.json(distributor); 75 | } catch (error) { 76 | return c.json({ error: "Internal server error" }, 500); 77 | } 78 | }; 79 | 80 | export const deleteDistributorController = async (c: Context) => { 81 | try { 82 | const id = Number(c.req.param("id")); 83 | if (!Number.isInteger(id) || id <= 0) { 84 | return c.json({ error: "Invalid id" }, 400); 85 | } 86 | const [distributor] = await db 87 | .delete(distributorsTable) 88 | .where(eq(distributorsTable.id, id)) 89 | .returning(); 90 | if (!distributor) return c.json({ error: "Distributor not found" }, 404); 91 | return c.json(distributor); 92 | } catch (error) { 93 | return c.json({ error: "Internal server error" }, 500); 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /apps/api/src/controllers/v1/game-modes.controller.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | 3 | export const getAllGameModesController = async (c: Context) => { 4 | return c.json({ message: "Game modes endpoint not implemented" }, 501); 5 | }; 6 | -------------------------------------------------------------------------------- /apps/api/src/controllers/v1/game-platforms.controller.ts: -------------------------------------------------------------------------------- 1 | // v1: game-platforms controller (boş şablon) 2 | -------------------------------------------------------------------------------- /apps/api/src/controllers/v1/game-ranks.controller.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | import { eq } from "drizzle-orm"; 3 | import { db } from "../../db"; 4 | import { gameRanks } from "../../db/schemas/game-ranks.drizzle"; 5 | import { createGameRankSchema, updateGameRankSchema } from "@repo/shared"; 6 | 7 | export const getAllGameRanksController = async (c: Context) => { 8 | try { 9 | const ranks = await db.select().from(gameRanks); 10 | return c.json(ranks); 11 | } catch (error) { 12 | return c.json({ error: "Internal server error" }, 500); 13 | } 14 | }; 15 | 16 | export const getGameRankByIdController = async (c: Context) => { 17 | try { 18 | const id = Number(c.req.param("id")); 19 | if (!Number.isInteger(id) || id <= 0) { 20 | return c.json({ error: "Invalid id" }, 400); 21 | } 22 | const rank = await db.select().from(gameRanks).where(eq(gameRanks.id, id)); 23 | if (rank.length === 0) return c.json({ error: "Game rank not found" }, 404); 24 | return c.json(rank[0]); 25 | } catch (error) { 26 | return c.json({ error: "Internal server error" }, 500); 27 | } 28 | }; 29 | 30 | export const createGameRankController = async (c: Context) => { 31 | try { 32 | const data = await c.req.json(); 33 | const result = createGameRankSchema.safeParse(data); 34 | if (!result.success) { 35 | return c.json({ error: result.error.flatten().fieldErrors }, 400); 36 | } 37 | const [rank] = await db.insert(gameRanks).values(result.data).returning(); 38 | return c.json(rank, 201); 39 | } catch (error) { 40 | return c.json({ error: "Internal server error" }, 500); 41 | } 42 | }; 43 | 44 | export const updateGameRankController = async (c: Context) => { 45 | try { 46 | const id = Number(c.req.param("id")); 47 | if (!Number.isInteger(id) || id <= 0) { 48 | return c.json({ error: "Invalid id" }, 400); 49 | } 50 | const data = await c.req.json(); 51 | const result = updateGameRankSchema.safeParse(data); 52 | if (!result.success) { 53 | return c.json({ error: result.error.flatten().fieldErrors }, 400); 54 | } 55 | if (Object.keys(result.data).length === 0) { 56 | return c.json({ error: "No valid fields provided for update" }, 400); 57 | } 58 | const [rank] = await db 59 | .update(gameRanks) 60 | .set(result.data) 61 | .where(eq(gameRanks.id, id)) 62 | .returning(); 63 | if (!rank) return c.json({ error: "Game rank not found" }, 404); 64 | return c.json(rank); 65 | } catch (error) { 66 | return c.json({ error: "Internal server error" }, 500); 67 | } 68 | }; 69 | 70 | export const deleteGameRankController = async (c: Context) => { 71 | try { 72 | const id = Number(c.req.param("id")); 73 | if (!Number.isInteger(id) || id <= 0) { 74 | return c.json({ error: "Invalid id" }, 400); 75 | } 76 | const [rank] = await db 77 | .delete(gameRanks) 78 | .where(eq(gameRanks.id, id)) 79 | .returning(); 80 | if (!rank) return c.json({ error: "Game rank not found" }, 404); 81 | return c.json(rank); 82 | } catch (error) { 83 | return c.json({ error: "Internal server error" }, 500); 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /apps/api/src/controllers/v1/game-regions.controller.ts: -------------------------------------------------------------------------------- 1 | // v1: game-regions controller (boş şablon) 2 | -------------------------------------------------------------------------------- /apps/api/src/controllers/v1/games.controller.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | import { eq } from "drizzle-orm"; 3 | import { db } from "../../db"; 4 | import { gamesTable } from "../../db/schemas/games.drizzle"; 5 | import { 6 | createGameSchema, 7 | updateGameSchema 8 | } from "@repo/shared"; 9 | 10 | export const getAllGamesController = async (c: Context) => { 11 | try { 12 | const games = await db.select().from(gamesTable); 13 | return c.json(games); 14 | } catch (error) { 15 | return c.json({ error: "Internal server error" }, 500); 16 | } 17 | }; 18 | 19 | export const getGameByIdController = async (c: Context) => { 20 | try { 21 | const id = Number(c.req.param("id")); 22 | if (!Number.isInteger(id) || id <= 0) { 23 | return c.json({ error: "Invalid id" }, 400); 24 | } 25 | const game = await db.select().from(gamesTable).where(eq(gamesTable.id, id)); 26 | if (game.length === 0) return c.json({ error: "Game not found" }, 404); 27 | return c.json(game[0]); 28 | } catch (error) { 29 | return c.json({ error: "Internal server error" }, 500); 30 | } 31 | }; 32 | 33 | export const createGameController = async (c: Context) => { 34 | try { 35 | const data = await c.req.json(); 36 | const result = createGameSchema.safeParse(data); 37 | if (!result.success) { 38 | return c.json({ error: result.error.flatten().fieldErrors }, 400); 39 | } 40 | const [game] = await db.insert(gamesTable).values(result.data).returning(); 41 | return c.json(game, 201); 42 | } catch (error) { 43 | return c.json({ error: "Internal server error" }, 500); 44 | } 45 | }; 46 | 47 | export const updateGameController = async (c: Context) => { 48 | try { 49 | const id = Number(c.req.param("id")); 50 | if (!Number.isInteger(id) || id <= 0) { 51 | return c.json({ error: "Invalid id" }, 400); 52 | } 53 | const data = await c.req.json(); 54 | const result = updateGameSchema.safeParse(data); 55 | if (!result.success) { 56 | return c.json({ error: result.error.flatten().fieldErrors }, 400); 57 | } 58 | if (Object.keys(result.data).length === 0) { 59 | return c.json({ error: "No valid fields provided for update" }, 400); 60 | } 61 | const [game] = await db 62 | .update(gamesTable) 63 | .set(result.data) 64 | .where(eq(gamesTable.id, id)) 65 | .returning(); 66 | if (!game) return c.json({ error: "Game not found" }, 404); 67 | return c.json(game); 68 | } catch (error) { 69 | return c.json({ error: "Internal server error" }, 500); 70 | } 71 | }; 72 | 73 | export const deleteGameController = async (c: Context) => { 74 | try { 75 | const id = Number(c.req.param("id")); 76 | if (!Number.isInteger(id) || id <= 0) { 77 | return c.json({ error: "Invalid id" }, 400); 78 | } 79 | const [game] = await db 80 | .delete(gamesTable) 81 | .where(eq(gamesTable.id, id)) 82 | .returning(); 83 | if (!game) return c.json({ error: "Game not found" }, 404); 84 | return c.json(game); 85 | } catch (error) { 86 | return c.json({ error: "Internal server error" }, 500); 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /apps/api/src/controllers/v1/languages.controller.ts: -------------------------------------------------------------------------------- 1 | // v1: languages controller (boş şablon) 2 | -------------------------------------------------------------------------------- /apps/api/src/controllers/v1/lobbies.controller.ts: -------------------------------------------------------------------------------- 1 | // v1: lobbies controller (boş şablon) 2 | -------------------------------------------------------------------------------- /apps/api/src/controllers/v1/main.controller.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | 3 | export const homeController = (c: Context) => { 4 | return c.text("Hello Hono!"); 5 | }; 6 | 7 | -------------------------------------------------------------------------------- /apps/api/src/controllers/v1/platforms.controller.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | import { eq } from "drizzle-orm"; 3 | import { db } from "../../db"; 4 | import { platforms } from "../../db/schemas/platforms.drizzle"; 5 | import { createPlatformSchema, updatePlatformSchema } from "@repo/shared"; 6 | import { convertBigIntToNumber } from "../../utils/bigint-serializer"; 7 | 8 | export const getAllPlatformsController = async (c: Context) => { 9 | try { 10 | const result = await db.select().from(platforms); 11 | return c.json(convertBigIntToNumber(result)); 12 | } catch { 13 | return c.json({ error: "Internal server error" }, 500); 14 | } 15 | }; 16 | 17 | export const getPlatformByIdController = async (c: Context) => { 18 | try { 19 | const idParam = c.req.param("id"); 20 | if (!idParam || !/^\d+$/.test(idParam)) { 21 | return c.json({ error: "Invalid id" }, 400); 22 | } 23 | const id = BigInt(idParam); 24 | const result = await db 25 | .select() 26 | .from(platforms) 27 | .where(eq(platforms.id, id)); 28 | if (result.length === 0) 29 | return c.json({ error: "Platform not found" }, 404); 30 | 31 | return c.json(convertBigIntToNumber(result[0])); 32 | } catch { 33 | return c.json({ error: "Internal server error" }, 500); 34 | } 35 | }; 36 | 37 | export const createPlatformController = async (c: Context) => { 38 | try { 39 | const data = await c.req.json(); 40 | const result = createPlatformSchema.safeParse(data); 41 | if (!result.success) { 42 | return c.json({ error: result.error.flatten().fieldErrors }, 400); 43 | } 44 | const [platform] = await db 45 | .insert(platforms) 46 | .values(result.data) 47 | .returning(); 48 | return c.json(convertBigIntToNumber(platform), 201); 49 | } catch (error) { 50 | return c.json({ error: "Internal server error" }, 500); 51 | } 52 | }; 53 | 54 | export const updatePlatformController = async (c: Context) => { 55 | try { 56 | const idParam = c.req.param("id"); 57 | if (!idParam || !/^\d+$/.test(idParam)) { 58 | return c.json({ error: "Invalid id" }, 400); 59 | } 60 | const id = BigInt(idParam); 61 | const data = await c.req.json(); 62 | const result = updatePlatformSchema.safeParse(data); 63 | if (!result.success) { 64 | return c.json({ error: result.error.flatten().fieldErrors }, 400); 65 | } 66 | if (Object.keys(result.data).length === 0) { 67 | return c.json({ error: "No data provided" }, 400); 68 | } 69 | const [platform] = await db 70 | .update(platforms) 71 | .set(result.data) 72 | .where(eq(platforms.id, id)) 73 | .returning(); 74 | if (!platform) return c.json({ error: "Platform not found" }, 404); 75 | return c.json(convertBigIntToNumber(platform)); 76 | } catch { 77 | return c.json({ error: "Internal server error" }, 500); 78 | } 79 | }; 80 | 81 | export const deletePlatformController = async (c: Context) => { 82 | try { 83 | const idParam = c.req.param("id"); 84 | if (!idParam || !/^\d+$/.test(idParam)) { 85 | return c.json({ error: "Invalid id" }, 400); 86 | } 87 | const id = BigInt(idParam); 88 | const [platform] = await db 89 | .delete(platforms) 90 | .where(eq(platforms.id, id)) 91 | .returning(); 92 | if (!platform) return c.json({ error: "Platform not found" }, 404); 93 | return c.json(convertBigIntToNumber(platform)); 94 | } catch { 95 | return c.json({ error: "Internal server error" }, 500); 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /apps/api/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { drizzle } from 'drizzle-orm/node-postgres'; 3 | import { Pool } from 'pg'; 4 | import * as schemas from "./schemas/schema"; 5 | 6 | const pool = new Pool({ connectionString: process.env.DATABASE_URL }); 7 | 8 | export const db = drizzle(pool, { schema: { ...schemas } }); -------------------------------------------------------------------------------- /apps/api/src/db/schemas/distributors.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { integer, pgTable, varchar, text, index, timestamp } from "drizzle-orm/pg-core"; 2 | 3 | export const distributorsTable = pgTable("distributors", { 4 | id: integer().primaryKey().generatedAlwaysAsIdentity(), 5 | name: varchar({ length: 255 }).notNull(), 6 | description: text(), 7 | website: varchar({ length: 255 }), 8 | logo: varchar({ length: 255 }), 9 | createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), 10 | updatedAt: timestamp('updated_at', { withTimezone: true }), 11 | deletedAt: timestamp('deleted_at', { withTimezone: true }), 12 | }, 13 | (table) => ({ 14 | nameIndex: index("distributors_name_idx").on(table.name), 15 | })); -------------------------------------------------------------------------------- /apps/api/src/db/schemas/event-attendees.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | pgTable, 3 | bigint, 4 | timestamp, 5 | index, 6 | foreignKey, 7 | } from "drizzle-orm/pg-core"; 8 | import { events } from "./events.drizzle"; 9 | import { userProfiles } from "./user-profile.drizzle"; 10 | 11 | export const eventAttendees = pgTable( 12 | "event_attendees", 13 | { 14 | id: bigint("id", { mode: "bigint" }) 15 | .primaryKey() 16 | .generatedAlwaysAsIdentity(), 17 | createdAt: timestamp("created_at", { withTimezone: true }) 18 | .notNull() 19 | .defaultNow(), 20 | updatedAt: timestamp("updated_at", { withTimezone: true }), 21 | deletedAt: timestamp("deleted_at", { withTimezone: true }), 22 | 23 | eventId: bigint("event_id", { mode: "bigint" }).notNull(), 24 | userProfileId: bigint("user_profile_id", { mode: "bigint" }).notNull(), 25 | joinedAt: timestamp("joined_at", { withTimezone: true }) 26 | .notNull() 27 | .defaultNow(), 28 | }, 29 | (table) => ({ 30 | eventIdIndex: index("event_attendees_event_id_idx").on(table.eventId), 31 | userProfileIdIndex: index("event_attendees_user_profile_id_idx").on( 32 | table.userProfileId, 33 | ), 34 | 35 | eventIdFk: foreignKey({ 36 | columns: [table.eventId], 37 | foreignColumns: [events.id], 38 | name: "fk_event_attendees_event_id", 39 | }), 40 | 41 | userProfileIdFk: foreignKey({ 42 | columns: [table.userProfileId], 43 | foreignColumns: [userProfiles.id], 44 | name: "fk_event_attendees_user_profile_id", 45 | }), 46 | }), 47 | ); 48 | -------------------------------------------------------------------------------- /apps/api/src/db/schemas/event-invitations.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | pgTable, 3 | bigint, 4 | timestamp, 5 | pgEnum, 6 | index, 7 | foreignKey, 8 | } from "drizzle-orm/pg-core"; 9 | import { events } from "./events.drizzle"; 10 | import { userProfiles } from "./user-profile.drizzle"; 11 | 12 | export const invitationStatusEnum = pgEnum("invitation_status", [ 13 | "pending", 14 | "accepted", 15 | "declined", 16 | ]); 17 | 18 | export const eventInvitations = pgTable( 19 | "event_invitations", 20 | { 21 | id: bigint("id", { mode: "bigint" }) 22 | .primaryKey() 23 | .generatedAlwaysAsIdentity(), 24 | createdAt: timestamp("created_at", { withTimezone: true }) 25 | .notNull() 26 | .defaultNow(), 27 | updatedAt: timestamp("updated_at", { withTimezone: true }), 28 | deletedAt: timestamp("deleted_at", { withTimezone: true }), 29 | 30 | inviterId: bigint("inviter_id", { mode: "bigint" }).notNull(), 31 | inviteeId: bigint("invitee_id", { mode: "bigint" }).notNull(), 32 | eventId: bigint("event_id", { mode: "bigint" }).notNull(), 33 | sentAt: timestamp("sent_at", { withTimezone: true }).notNull().defaultNow(), 34 | respondedAt: timestamp("responded_at", { withTimezone: true }), 35 | status: invitationStatusEnum("status").notNull().default("pending"), 36 | }, 37 | (table) => ({ 38 | eventIdIndex: index("event_invitations_event_id_idx").on(table.eventId), 39 | inviterIdIndex: index("event_invitations_inviter_id_idx").on( 40 | table.inviterId, 41 | ), 42 | inviteeIdIndex: index("event_invitations_invitee_id_idx").on( 43 | table.inviteeId, 44 | ), 45 | 46 | eventIdFk: foreignKey({ 47 | columns: [table.eventId], 48 | foreignColumns: [events.id], 49 | name: "fk_event_invitations_event_id", 50 | }), 51 | inviterIdFk: foreignKey({ 52 | columns: [table.inviterId], 53 | foreignColumns: [userProfiles.id], 54 | name: "fk_event_invitations_inviter_id", 55 | }), 56 | inviteeIdFk: foreignKey({ 57 | columns: [table.inviteeId], 58 | foreignColumns: [userProfiles.id], 59 | name: "fk_event_invitations_invitee_id", 60 | }), 61 | }), 62 | ); 63 | -------------------------------------------------------------------------------- /apps/api/src/db/schemas/events.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | pgTable, 3 | bigint, 4 | timestamp, 5 | varchar, 6 | text, 7 | boolean, 8 | integer, 9 | index, 10 | foreignKey, 11 | pgEnum, 12 | } from "drizzle-orm/pg-core"; 13 | import { userProfiles } from "./user-profile.drizzle"; 14 | 15 | export const eventTypeEnum = pgEnum("event_type", [ 16 | "meetup", 17 | "tournament", 18 | "other", 19 | ]); 20 | 21 | export const events = pgTable( 22 | "events", 23 | { 24 | id: bigint("id", { mode: "bigint" }) 25 | .primaryKey() 26 | .generatedAlwaysAsIdentity(), 27 | createdAt: timestamp("created_at", { withTimezone: true }) 28 | .notNull() 29 | .defaultNow(), 30 | updatedAt: timestamp("updated_at", { withTimezone: true }), 31 | deletedAt: timestamp("deleted_at", { withTimezone: true }), 32 | 33 | title: varchar("title", { length: 150 }).notNull(), 34 | description: text("description"), 35 | startTime: timestamp("start_time", { withTimezone: true }).notNull(), 36 | endTime: timestamp("end_time", { withTimezone: true }).notNull(), 37 | place: varchar("place", { length: 255 }), 38 | isOnline: boolean("is_online").notNull().default(false), 39 | imageUrl: varchar("image_url", { length: 255 }), 40 | isOfficial: boolean("is_official").notNull().default(false), 41 | creatorId: bigint("creator_id", { mode: "bigint" }).notNull(), 42 | minAgeRestriction: integer("min_age_restriction"), 43 | attendeesCount: integer("attendees_count").notNull().default(0), 44 | languageId: bigint("language_id", { mode: "bigint" }), 45 | countryId: bigint("country_id", { mode: "bigint" }), 46 | cityId: bigint("city_id", { mode: "bigint" }), 47 | eventType: eventTypeEnum("event_type").notNull(), 48 | }, 49 | (table) => ({ 50 | creatorIdIndex: index("events_creator_id_idx").on(table.creatorId), 51 | startTimeIndex: index("events_start_time_idx").on(table.startTime), 52 | eventTypeIndex: index("events_event_type_idx").on(table.eventType), 53 | 54 | creatorIdFk: foreignKey({ 55 | columns: [table.creatorId], 56 | foreignColumns: [userProfiles.id], 57 | name: "fk_events_creator_id", 58 | }), 59 | }), 60 | ); 61 | -------------------------------------------------------------------------------- /apps/api/src/db/schemas/game-distributors.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, bigint, timestamp, index, foreignKey } from "drizzle-orm/pg-core"; 2 | import { gamesTable } from "./games.drizzle"; 3 | import { distributorsTable } from "./distributors.drizzle"; 4 | 5 | export const gameDistributors = pgTable( 6 | "game_distributors", 7 | { 8 | id: bigint("id", { mode: "bigint" }) 9 | .primaryKey() 10 | .generatedAlwaysAsIdentity(), 11 | createdAt: timestamp("created_at", { withTimezone: true }) 12 | .notNull() 13 | .defaultNow(), 14 | updatedAt: timestamp("updated_at", { withTimezone: true }), 15 | deletedAt: timestamp("deleted_at", { withTimezone: true }), 16 | 17 | gameId: bigint("game_id", { mode: "bigint" }).notNull(), 18 | distributorId: bigint("distributor_id", { mode: "bigint" }).notNull(), 19 | }, 20 | (table) => ({ 21 | gameIdIndex: index("game_distributors_game_id_idx").on(table.gameId), 22 | distributorIdIndex: index("game_distributors_distributor_id_idx").on(table.distributorId), 23 | 24 | gameIdFk: foreignKey({ 25 | columns: [table.gameId], 26 | foreignColumns: [gamesTable.id], 27 | name: "fk_game_distributors_game_id", 28 | }), 29 | 30 | distributorIdFk: foreignKey({ 31 | columns: [table.distributorId], 32 | foreignColumns: [distributorsTable.id], 33 | name: "fk_game_distributors_distributor_id", 34 | }), 35 | }), 36 | ); 37 | -------------------------------------------------------------------------------- /apps/api/src/db/schemas/game-modes.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, bigint, integer, timestamp, text, varchar, index, foreignKey, unique } from 'drizzle-orm/pg-core'; 2 | import { gamesTable } from "./games.drizzle"; 3 | 4 | // GameMode table 5 | export const gameModes = pgTable('game_modes', { 6 | id: bigint('id', { mode: 'bigint' }).primaryKey().generatedAlwaysAsIdentity(), 7 | createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), 8 | updatedAt: timestamp('updated_at', { withTimezone: true }), 9 | deletedAt: timestamp('deleted_at', { withTimezone: true }), 10 | 11 | name: varchar('name', { length: 150 }).notNull(), 12 | description: text('description'), 13 | order: integer('order').notNull(), 14 | 15 | gameId: bigint('game_id', { mode: 'bigint' }).notNull(), 16 | 17 | minTeamSize: integer('min_team_size').notNull(), 18 | maxTeamSize: integer('max_team_size').notNull(), 19 | }, (table) => ({ 20 | gameIdIndex: index('game_modes_game_id_idx').on(table.gameId), 21 | nameIndex: index('game_modes_name_idx').on(table.name), 22 | 23 | gameIdFk: foreignKey({ 24 | columns: [table.gameId], 25 | foreignColumns: [gamesTable.id], 26 | name: 'fk_game_modes_game_id' 27 | }), 28 | })); 29 | -------------------------------------------------------------------------------- /apps/api/src/db/schemas/game-platforms.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, bigint, timestamp, varchar, index, unique, foreignKey } from "drizzle-orm/pg-core"; 2 | import { gamesTable } from "./games.drizzle"; 3 | import { platforms } from "./platforms.drizzle"; 4 | 5 | 6 | // GamePlatform junction table 7 | export const gamePlatforms = pgTable('game_platforms', { 8 | id: bigint('id', { mode: 'bigint' }).primaryKey().generatedAlwaysAsIdentity(), 9 | createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), 10 | updatedAt: timestamp('updated_at', { withTimezone: true }), 11 | deletedAt: timestamp('deleted_at', { withTimezone: true }), 12 | 13 | gameId: bigint('game_id', { mode: 'bigint' }).notNull(), 14 | platformId: bigint('platform_id', { mode: 'bigint' }).notNull(), 15 | }, (table) => ({ 16 | gameIdIndex: index('game_platforms_game_id_idx').on(table.gameId), 17 | platformIdIndex: index('game_platforms_platform_id_idx').on(table.platformId), 18 | 19 | // Unique constraint to prevent duplicate game-platform combinations 20 | uniqueGamePlatform: unique('unique_game_platform').on(table.gameId, table.platformId), 21 | 22 | gameIdFk: foreignKey({ 23 | columns: [table.gameId], 24 | foreignColumns: [gamesTable.id], 25 | name: 'fk_game_platforms_game_id' 26 | }), 27 | 28 | platformIdFk: foreignKey({ 29 | columns: [table.platformId], 30 | foreignColumns: [platforms.id], 31 | name: 'fk_game_platforms_platform_id' 32 | }), 33 | })); -------------------------------------------------------------------------------- /apps/api/src/db/schemas/game-ranks.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, integer, timestamp, varchar, index, foreignKey } from "drizzle-orm/pg-core"; 2 | import { gamesTable } from "./games.drizzle"; 3 | 4 | 5 | // GameRank table 6 | export const gameRanks = pgTable('game_ranks', { 7 | id: integer().primaryKey().generatedAlwaysAsIdentity(), 8 | createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), 9 | updatedAt: timestamp('updated_at', { withTimezone: true }), 10 | deletedAt: timestamp('deleted_at', { withTimezone: true }), 11 | 12 | name: varchar('name', { length: 100 }).notNull(), 13 | image: varchar('image', { length: 255 }).notNull(), 14 | order: integer('order').notNull(), 15 | 16 | gameId: integer('game_id').notNull(), 17 | }, (table) => ({ 18 | gameIdIndex: index('game_ranks_game_id_idx').on(table.gameId), 19 | nameIndex: index('game_ranks_name_idx').on(table.name), 20 | 21 | gameIdFk: foreignKey({ 22 | columns: [table.gameId], 23 | foreignColumns: [gamesTable.id], 24 | name: 'fk_game_ranks_game_id' 25 | }), 26 | })); 27 | 28 | // Note: You'll need to import the gamesTable from your existing games schema file 29 | // import { gamesTable } from './games'; -------------------------------------------------------------------------------- /apps/api/src/db/schemas/games.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { integer, pgTable, varchar, text, index, timestamp } from "drizzle-orm/pg-core"; 2 | 3 | export const gamesTable = pgTable("games", { 4 | id: integer().primaryKey().generatedAlwaysAsIdentity(), 5 | name: varchar({ length: 150 }).notNull(), 6 | description: text(), 7 | logo: varchar({ length: 255 }), 8 | createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), 9 | updatedAt: timestamp('updated_at', { withTimezone: true }), 10 | deletedAt: timestamp('deleted_at', { withTimezone: true }), 11 | }, 12 | (table) => ({ 13 | nameIndex: index("games_name_idx").on(table.name), 14 | })); -------------------------------------------------------------------------------- /apps/api/src/db/schemas/languages.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, bigint, timestamp, varchar, index } from "drizzle-orm/pg-core"; 2 | 3 | export const languages = pgTable( 4 | "languages", 5 | { 6 | id: bigint("id", { mode: "bigint" }) 7 | .primaryKey() 8 | .generatedAlwaysAsIdentity(), 9 | createdAt: timestamp("created_at", { withTimezone: true }) 10 | .notNull() 11 | .defaultNow(), 12 | updatedAt: timestamp("updated_at", { withTimezone: true }), 13 | deletedAt: timestamp("deleted_at", { withTimezone: true }), 14 | 15 | name: varchar("name", { length: 100 }).notNull(), 16 | code: varchar("code", { length: 10 }).notNull(), 17 | flagUrl: varchar("flag_url", { length: 255 }), 18 | }, 19 | (table) => ({ 20 | nameIndex: index("languages_name_idx").on(table.name), 21 | codeIndex: index("languages_code_idx").on(table.code), 22 | }), 23 | ); 24 | -------------------------------------------------------------------------------- /apps/api/src/db/schemas/lobbies.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | pgTable, 3 | bigint, 4 | integer, 5 | timestamp, 6 | varchar, 7 | text, 8 | boolean, 9 | index, 10 | pgEnum, 11 | foreignKey, 12 | } from "drizzle-orm/pg-core"; 13 | import { gamesTable } from "./games.drizzle"; 14 | import { gameModes } from "./game-modes.drizzle"; 15 | import { gameRanks } from "./game-ranks.drizzle"; 16 | import { userProfiles } from "./user-profile.drizzle"; 17 | 18 | export const lobbyTypeEnum = pgEnum("lobby_type", ["public", "private"]); 19 | 20 | export const lobbies = pgTable( 21 | "lobbies", 22 | { 23 | id: bigint("id", { mode: "bigint" }) 24 | .primaryKey() 25 | .generatedAlwaysAsIdentity(), 26 | createdAt: timestamp("created_at", { withTimezone: true }) 27 | .notNull() 28 | .defaultNow(), 29 | updatedAt: timestamp("updated_at", { withTimezone: true }), 30 | deletedAt: timestamp("deleted_at", { withTimezone: true }), 31 | 32 | gameId: bigint("game_id", { mode: "bigint" }).notNull(), 33 | regionId: bigint("region_id", { mode: "bigint" }).notNull(), 34 | modeId: bigint("mode_id", { mode: "bigint" }).notNull(), 35 | minTeamSize: integer("min_team_size").notNull(), 36 | maxTeamSize: integer("max_team_size").notNull(), 37 | type: lobbyTypeEnum("type").notNull().default("public"), 38 | minRankId: bigint("min_rank_id", { mode: "bigint" }), 39 | maxRankId: bigint("max_rank_id", { mode: "bigint" }), 40 | isMicRequired: boolean("is_mic_required").notNull().default(false), 41 | creatorId: bigint("creator_id", { mode: "bigint" }).notNull(), 42 | ownerId: bigint("owner_id", { mode: "bigint" }).notNull(), 43 | note: text("note"), 44 | discordLink: varchar("discord_link", { length: 255 }), 45 | rowVersion: text("row_version").notNull().$defaultFn(() => "0"), 46 | }, 47 | (table) => ({ 48 | gameIdIndex: index("lobbies_game_id_idx").on(table.gameId), 49 | regionIdIndex: index("lobbies_region_id_idx").on(table.regionId), 50 | modeIdIndex: index("lobbies_mode_id_idx").on(table.modeId), 51 | creatorIdIndex: index("lobbies_creator_id_idx").on(table.creatorId), 52 | ownerIdIndex: index("lobbies_owner_id_idx").on(table.ownerId), 53 | 54 | gameIdFk: foreignKey({ 55 | columns: [table.gameId], 56 | foreignColumns: [gamesTable.id], 57 | name: "fk_lobbies_game_id", 58 | }), 59 | 60 | modeIdFk: foreignKey({ 61 | columns: [table.modeId], 62 | foreignColumns: [gameModes.id], 63 | name: "fk_lobbies_mode_id", 64 | }), 65 | 66 | minRankIdFk: foreignKey({ 67 | columns: [table.minRankId], 68 | foreignColumns: [gameRanks.id], 69 | name: "fk_lobbies_min_rank_id", 70 | }), 71 | 72 | maxRankIdFk: foreignKey({ 73 | columns: [table.maxRankId], 74 | foreignColumns: [gameRanks.id], 75 | name: "fk_lobbies_max_rank_id", 76 | }), 77 | 78 | creatorIdFk: foreignKey({ 79 | columns: [table.creatorId], 80 | foreignColumns: [userProfiles.id], 81 | name: "fk_lobbies_creator_id", 82 | }), 83 | 84 | ownerIdFk: foreignKey({ 85 | columns: [table.ownerId], 86 | foreignColumns: [userProfiles.id], 87 | name: "fk_lobbies_owner_id", 88 | }), 89 | }), 90 | ); 91 | -------------------------------------------------------------------------------- /apps/api/src/db/schemas/lobby-languages.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, bigint, timestamp, index, foreignKey } from "drizzle-orm/pg-core"; 2 | import { lobbies } from "./lobbies.drizzle"; 3 | import { languages } from "./languages.drizzle"; 4 | 5 | export const lobbyLanguages = pgTable( 6 | "lobby_languages", 7 | { 8 | id: bigint("id", { mode: "bigint" }) 9 | .primaryKey() 10 | .generatedAlwaysAsIdentity(), 11 | createdAt: timestamp("created_at", { withTimezone: true }) 12 | .notNull() 13 | .defaultNow(), 14 | updatedAt: timestamp("updated_at", { withTimezone: true }), 15 | deletedAt: timestamp("deleted_at", { withTimezone: true }), 16 | 17 | lobbyId: bigint("lobby_id", { mode: "bigint" }).notNull(), 18 | languageId: bigint("language_id", { mode: "bigint" }).notNull(), 19 | }, 20 | (table) => ({ 21 | lobbyIdIndex: index("lobby_languages_lobby_id_idx").on(table.lobbyId), 22 | languageIdIndex: index("lobby_languages_language_id_idx").on(table.languageId), 23 | 24 | lobbyIdFk: foreignKey({ 25 | columns: [table.lobbyId], 26 | foreignColumns: [lobbies.id], 27 | name: "fk_lobby_languages_lobby_id", 28 | }), 29 | 30 | languageIdFk: foreignKey({ 31 | columns: [table.languageId], 32 | foreignColumns: [languages.id], 33 | name: "fk_lobby_languages_language_id", 34 | }), 35 | }), 36 | ); 37 | -------------------------------------------------------------------------------- /apps/api/src/db/schemas/lobby-members.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, bigint, timestamp, boolean, index, foreignKey } from "drizzle-orm/pg-core"; 2 | import { lobbies } from "./lobbies.drizzle"; 3 | import { userProfiles } from "./user-profile.drizzle"; 4 | 5 | export const lobbyMembers = pgTable( 6 | "lobby_members", 7 | { 8 | id: bigint("id", { mode: "bigint" }) 9 | .primaryKey() 10 | .generatedAlwaysAsIdentity(), 11 | createdAt: timestamp("created_at", { withTimezone: true }) 12 | .notNull() 13 | .defaultNow(), 14 | updatedAt: timestamp("updated_at", { withTimezone: true }), 15 | deletedAt: timestamp("deleted_at", { withTimezone: true }), 16 | 17 | lobbyId: bigint("lobby_id", { mode: "bigint" }).notNull(), 18 | memberId: bigint("member_id", { mode: "bigint" }).notNull(), 19 | isAdmin: boolean("is_admin").notNull().default(false), 20 | }, 21 | (table) => ({ 22 | lobbyIdIndex: index("lobby_members_lobby_id_idx").on(table.lobbyId), 23 | memberIdIndex: index("lobby_members_member_id_idx").on(table.memberId), 24 | 25 | lobbyIdFk: foreignKey({ 26 | columns: [table.lobbyId], 27 | foreignColumns: [lobbies.id], 28 | name: "fk_lobby_members_lobby_id", 29 | }), 30 | 31 | memberIdFk: foreignKey({ 32 | columns: [table.memberId], 33 | foreignColumns: [userProfiles.id], 34 | name: "fk_lobby_members_member_id", 35 | }), 36 | }), 37 | ); 38 | -------------------------------------------------------------------------------- /apps/api/src/db/schemas/platforms.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, bigint, timestamp, varchar, index } from "drizzle-orm/pg-core"; 2 | 3 | // Platform table 4 | export const platforms = pgTable('platforms', { 5 | id: bigint('id', { mode: 'bigint' }).primaryKey().generatedAlwaysAsIdentity(), 6 | createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), 7 | updatedAt: timestamp('updated_at', { withTimezone: true }), 8 | deletedAt: timestamp('deleted_at', { withTimezone: true }), 9 | 10 | name: varchar('name', { length: 100 }).notNull(), 11 | }, (table) => ({ 12 | nameIndex: index('platforms_name_idx').on(table.name), 13 | })); 14 | 15 | -------------------------------------------------------------------------------- /apps/api/src/db/schemas/schema.ts: -------------------------------------------------------------------------------- 1 | // Re-export all schemas from individual files 2 | export * from "./user-profile.drizzle"; 3 | export * from "./games.drizzle"; 4 | export * from "./distributors.drizzle"; 5 | export * from "./game-platforms.drizzle"; 6 | export * from "./game-ranks.drizzle"; 7 | export * from "./game-modes.drizzle"; 8 | export * from "./platforms.drizzle"; 9 | export * from "./events.drizzle"; 10 | export * from "./event-attendees.drizzle"; 11 | export * from "./event-invitations.drizzle"; 12 | export * from "./languages.drizzle"; 13 | export * from "./lobbies.drizzle"; 14 | export * from "./lobby-languages.drizzle"; 15 | export * from "./lobby-members.drizzle"; 16 | export * from "./game-distributors.drizzle"; 17 | -------------------------------------------------------------------------------- /apps/api/src/db/schemas/user-profile.drizzle.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, bigint, timestamp, text, pgEnum,varchar } from 'drizzle-orm/pg-core'; 2 | 3 | // Enums 4 | export const genderEnum = pgEnum('gender', ['male', 'female', 'unknown']); 5 | export const regionTypeEnum = pgEnum('region_type', ['north_america','south_america','europe', 'asia', 'oceania', 'middle_east', 'africa', 'russia_cis', 'unknown']); 6 | 7 | 8 | // UserProfile table 9 | export const userProfiles = pgTable('user_profiles', { 10 | 11 | id: bigint('id', { mode: 'bigint' }).primaryKey().generatedAlwaysAsIdentity(), 12 | userKeycloakId: varchar('user_keycloak_id', { length: 100 }).notNull().unique(), 13 | createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), 14 | updatedAt: timestamp('updated_at', { withTimezone: true }), 15 | deletedAt: timestamp('deleted_at', { withTimezone: true }), 16 | 17 | birthDate: timestamp('birth_date', { withTimezone: true }), 18 | userName: varchar('user_name', { length: 50 }).notNull().unique(), 19 | firstName: varchar('first_name', { length: 60 }), 20 | lastName: varchar('last_name', { length: 60 }), 21 | profileImageUrl: varchar('profile_image_url', { length: 255 }), 22 | bannerImageUrl: varchar('banner_image_url', { length: 255 }), 23 | bio: text('bio'), 24 | 25 | // Enums 26 | gender: genderEnum('gender').notNull().default('unknown'), 27 | regionType: regionTypeEnum('region_type').notNull().default('unknown'), 28 | 29 | // Timestamps 30 | lastOnline: timestamp('last_online', { withTimezone: true }).notNull().defaultNow(), 31 | 32 | // Optimistic concurrency control 33 | rowVersion: text('row_version').notNull().$defaultFn(() => '0'), 34 | }); 35 | -------------------------------------------------------------------------------- /apps/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono'; 2 | import { cors } from 'hono/cors'; 3 | import { logger } from 'hono/logger'; 4 | import { swaggerUI } from '@hono/swagger-ui'; 5 | import router from './routes'; 6 | import { versionMiddleware } from './middleware/version'; 7 | import { deprecationMiddleware } from './middleware/deprecation'; 8 | import YAML from 'yamljs'; 9 | import path from 'path'; 10 | 11 | const app = new Hono(); 12 | 13 | app.use('*', logger()); 14 | app.use('*', cors({ 15 | origin: ['http://localhost:3000', 'http://localhost:3001'], 16 | allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 17 | allowHeaders: ['Content-Type', 'Authorization'], 18 | })); 19 | 20 | app.use('/api/*', versionMiddleware); 21 | app.use('/api/*', deprecationMiddleware); 22 | 23 | app.get('/docs', swaggerUI({ url: '/docs/openapi.json' })); 24 | 25 | app.get('/docs/openapi.json', (c) => { 26 | const openapiPath = path.join(process.cwd(), 'openapi.yaml'); 27 | const openapiDoc = YAML.load(openapiPath); 28 | return c.json(openapiDoc); 29 | }); 30 | 31 | app.route('/', router); 32 | 33 | app.onError((err, c) => { 34 | console.error('API Error:', err); 35 | return c.json({ 36 | error: 'Internal Server Error', 37 | version: (c.get as (key: string) => unknown)('version') || 'unknown', 38 | }, 500); 39 | }); 40 | 41 | export default app; 42 | -------------------------------------------------------------------------------- /apps/api/src/lib/zod-schemas/game-ranks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const createGameRankSchema = z.object({ 4 | name: z.string().min(1, { message: "Name is required" }).max(100, { message: "Name must be 100 characters or less" }), 5 | image: z.string().min(1, { message: "Image is required" }).max(255, { message: "Image must be 255 characters or less" }), 6 | order: z.number().int({ message: "Order must be an integer" }).min(0, { message: "Order must be non-negative" }), 7 | gameId: z.number().int({ message: "Game ID must be an integer" }).positive({ message: "Game ID must be positive" }), 8 | }); 9 | 10 | export const updateGameRankSchema = z.object({ 11 | name: z.string().min(1, { message: "Name cannot be empty" }).max(100, { message: "Name must be 100 characters or less" }).optional(), 12 | image: z.string().min(1, { message: "Image cannot be empty" }).max(255, { message: "Image must be 255 characters or less" }).optional(), 13 | order: z.number().int({ message: "Order must be an integer" }).min(0, { message: "Order must be non-negative" }).optional(), 14 | gameId: z.number().int({ message: "Game ID must be an integer" }).positive({ message: "Game ID must be positive" }).optional(), 15 | }); 16 | -------------------------------------------------------------------------------- /apps/api/src/middleware/deprecation.ts: -------------------------------------------------------------------------------- 1 | import { Context, Next } from 'hono' 2 | 3 | interface DeprecationConfig { 4 | version: string 5 | deprecatedAt: Date 6 | sunsetAt: Date 7 | message?: string 8 | } 9 | 10 | const deprecatedVersions: DeprecationConfig[] = [ 11 | // Örnek: v1 gelecekte deprecated olacak 12 | // { 13 | // version: 'v1', 14 | // deprecatedAt: new Date('2024-12-01'), 15 | // sunsetAt: new Date('2025-06-01'), 16 | // message: 'Lütfen v2 API’ye geçiş yapın.' 17 | // } 18 | ] 19 | 20 | export async function deprecationMiddleware(c: Context, next: Next) { 21 | const versionContext = c.get('version') 22 | 23 | if (versionContext) { 24 | const deprecated = deprecatedVersions.find( 25 | d => d.version === `v${versionContext.majorVersion}` 26 | ) 27 | 28 | if (deprecated) { 29 | c.header('X-API-Deprecated', 'true') 30 | c.header('X-API-Deprecation-Date', deprecated.deprecatedAt.toISOString()) 31 | c.header('X-API-Sunset-Date', deprecated.sunsetAt.toISOString()) 32 | 33 | if (deprecated.message) { 34 | c.header('X-API-Deprecation-Message', deprecated.message) 35 | } 36 | } 37 | } 38 | 39 | await next() 40 | } 41 | -------------------------------------------------------------------------------- /apps/api/src/middleware/version.ts: -------------------------------------------------------------------------------- 1 | import { Context, Next } from 'hono' 2 | 3 | export interface VersionContext { 4 | version: string 5 | majorVersion: number 6 | minorVersion: number 7 | } 8 | 9 | export async function versionMiddleware(c: Context, next: Next) { 10 | const path = c.req.path 11 | const versionMatch = path.match(/\/api\/v(\d+)(?:\.(\d+))?/) 12 | 13 | if (versionMatch) { 14 | const majorVersion = parseInt(versionMatch[1], 10) 15 | const minorVersion = versionMatch[2] ? parseInt(versionMatch[2], 10) : 0 16 | const version = `v${majorVersion}.${minorVersion}` 17 | 18 | c.set('version', { 19 | version, 20 | majorVersion, 21 | minorVersion, 22 | } as VersionContext) 23 | } else { 24 | // Default to v1 if no version specified 25 | c.set('version', { 26 | version: 'v1.0', 27 | majorVersion: 1, 28 | minorVersion: 0, 29 | } as VersionContext) 30 | } 31 | 32 | await next() 33 | } 34 | -------------------------------------------------------------------------------- /apps/api/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { homeController } from '../controllers/v1/main.controller' 3 | import v1Router from './v1' 4 | 5 | const router = new Hono() 6 | 7 | router.get('/', homeController) 8 | router.get('/health', homeController) 9 | 10 | router.route('/api/v1', v1Router) 11 | 12 | router.get('/api/*', (c) => { 13 | const path = c.req.path.replace(/^\/api\//, '/api/v1/') 14 | return c.redirect(path, 301) 15 | }) 16 | 17 | export default router 18 | -------------------------------------------------------------------------------- /apps/api/src/routes/v1/distributors.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { 3 | getAllDistributorsController, 4 | getDistributorByIdController, 5 | createDistributorController, 6 | updateDistributorController, 7 | deleteDistributorController, 8 | } from "../../controllers/v1/distributors.controller"; 9 | 10 | const distributors = new Hono(); 11 | 12 | distributors.get("/", getAllDistributorsController); 13 | distributors.get("/:id", getDistributorByIdController); 14 | distributors.post("/", createDistributorController); 15 | distributors.put("/:id", updateDistributorController); 16 | distributors.delete("/:id", deleteDistributorController); 17 | 18 | export default distributors; 19 | -------------------------------------------------------------------------------- /apps/api/src/routes/v1/event-attendees.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { 3 | getEventAttendees, 4 | getEventAttendeeById, 5 | getEventAttendeesByEvent, 6 | createEventAttendee, 7 | deleteEventAttendee, 8 | } from "../../controllers/v1/event-attendees.controller"; 9 | 10 | const eventAttendeesRouter = new Hono(); 11 | 12 | eventAttendeesRouter.get("/", getEventAttendees); 13 | eventAttendeesRouter.get("/:id", getEventAttendeeById); 14 | eventAttendeesRouter.post("/", createEventAttendee); 15 | eventAttendeesRouter.delete("/:id", deleteEventAttendee); 16 | eventAttendeesRouter.get("/events/:eventId/attendees", getEventAttendeesByEvent); 17 | 18 | export default eventAttendeesRouter; 19 | -------------------------------------------------------------------------------- /apps/api/src/routes/v1/event-invitations.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { 3 | getEventInvitations, 4 | getEventInvitationById, 5 | getUserInvitations, 6 | getEventInvitationsByEvent, 7 | createEventInvitation, 8 | respondToInvitation, 9 | deleteEventInvitation, 10 | } from "../../controllers/v1/event-invitations.controller"; 11 | import { zValidator } from "@hono/zod-validator"; 12 | import { z } from "zod"; 13 | import { 14 | createEventInvitationSchema, 15 | respondToInvitationSchema, 16 | getEventInvitationsSchema, 17 | } from "@repo/shared"; 18 | 19 | const eventInvitationsRouter = new Hono(); 20 | const idParamSchema = z.object({ 21 | id: z.string().regex(/^\d+$/).transform(Number), 22 | }); 23 | 24 | eventInvitationsRouter.get( 25 | "/", 26 | zValidator("query", getEventInvitationsSchema), 27 | getEventInvitations 28 | ); 29 | eventInvitationsRouter.get( 30 | "/:id", 31 | zValidator("param", idParamSchema), 32 | getEventInvitationById 33 | ); 34 | eventInvitationsRouter.post( 35 | "/", 36 | zValidator("json", createEventInvitationSchema), 37 | createEventInvitation 38 | ); 39 | eventInvitationsRouter.put( 40 | "/:id/respond", 41 | zValidator("json", respondToInvitationSchema), 42 | respondToInvitation 43 | ); 44 | eventInvitationsRouter.delete("/:id", deleteEventInvitation); 45 | eventInvitationsRouter.get( 46 | "/users/:userId/invitations", 47 | zValidator("query", getEventInvitationsSchema), 48 | getUserInvitations 49 | ); 50 | eventInvitationsRouter.get( 51 | "/events/:eventId/invitations", 52 | zValidator("query", getEventInvitationsSchema), 53 | getEventInvitationsByEvent 54 | ); 55 | 56 | export default eventInvitationsRouter; 57 | -------------------------------------------------------------------------------- /apps/api/src/routes/v1/game-ranks.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono'; 2 | import { 3 | getAllGameRanksController, 4 | getGameRankByIdController, 5 | createGameRankController, 6 | updateGameRankController, 7 | deleteGameRankController, 8 | } from '../../controllers/v1/game-ranks.controller'; 9 | 10 | const gameRanksRoutes = new Hono(); 11 | 12 | gameRanksRoutes.get('/', getAllGameRanksController); 13 | gameRanksRoutes.get('/:id', getGameRankByIdController); 14 | gameRanksRoutes.post('/', createGameRankController); 15 | gameRanksRoutes.put('/:id', updateGameRankController); 16 | gameRanksRoutes.delete('/:id', deleteGameRankController); 17 | 18 | export default gameRanksRoutes; 19 | -------------------------------------------------------------------------------- /apps/api/src/routes/v1/games.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono'; 2 | import { 3 | getAllGamesController, 4 | getGameByIdController, 5 | createGameController, 6 | updateGameController, 7 | deleteGameController, 8 | } from '../../controllers/v1/games.controller'; 9 | 10 | const games = new Hono(); 11 | 12 | games.get('/', getAllGamesController); 13 | games.get('/:id', getGameByIdController); 14 | games.post('/', createGameController); 15 | games.put('/:id', updateGameController); 16 | games.delete('/:id', deleteGameController); 17 | 18 | export default games; 19 | -------------------------------------------------------------------------------- /apps/api/src/routes/v1/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import gamesRoutes from './games' 3 | import platformsRoutes from './platforms' 4 | import distributorsRoutes from './distributors' 5 | import gameRanksRoutes from './game-ranks' 6 | import userProfilesRoutes from './user-profiles' 7 | import eventAttendeesRouter from './event-attendees' 8 | import eventInvitationsRouter from './event-invitations' 9 | 10 | const v1Router = new Hono() 11 | 12 | v1Router.route('/games', gamesRoutes) 13 | v1Router.route('/platforms', platformsRoutes) 14 | v1Router.route('/distributors', distributorsRoutes) 15 | v1Router.route('/game-ranks', gameRanksRoutes) 16 | v1Router.route('/user-profiles', userProfilesRoutes) 17 | v1Router.route('/event-attendees', eventAttendeesRouter) 18 | v1Router.route('/event-invitations', eventInvitationsRouter) 19 | 20 | 21 | export default v1Router 22 | -------------------------------------------------------------------------------- /apps/api/src/routes/v1/platforms.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono'; 2 | import { 3 | getAllPlatformsController, 4 | getPlatformByIdController, 5 | createPlatformController, 6 | updatePlatformController, 7 | deletePlatformController, 8 | } from '../../controllers/v1/platforms.controller'; 9 | 10 | const platforms = new Hono(); 11 | 12 | platforms.get('/', getAllPlatformsController); 13 | platforms.get('/:id', getPlatformByIdController); 14 | platforms.post('/', createPlatformController); 15 | platforms.put('/:id', updatePlatformController); 16 | platforms.delete('/:id', deletePlatformController); 17 | 18 | export default platforms; 19 | -------------------------------------------------------------------------------- /apps/api/src/routes/v1/user-profiles.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { 3 | createUserProfile, 4 | deleteUserProfile, 5 | getUserProfile, 6 | updateUserProfile 7 | } from '../../controllers/v1/user-profiles.controller' 8 | 9 | const userProfiles = new Hono() 10 | 11 | userProfiles.get('/:id', getUserProfile) 12 | userProfiles.post('/', createUserProfile) 13 | userProfiles.patch('/:id', updateUserProfile) 14 | userProfiles.delete('/:id', deleteUserProfile) 15 | 16 | export default userProfiles 17 | -------------------------------------------------------------------------------- /apps/api/src/types/yamljs.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'yamljs' { 2 | const YAML: { 3 | load(path: string): any; 4 | parse(yaml: string): any; 5 | stringify(obj: any, inline?: number, spaces?: number): string; 6 | dump(obj: any, inline?: number, spaces?: number): string; 7 | }; 8 | export = YAML; 9 | } 10 | -------------------------------------------------------------------------------- /apps/api/src/utils/bigint-serializer.ts: -------------------------------------------------------------------------------- 1 | type Serializable = bigint | string | number | boolean | null | undefined | Date | Serializable[] | { [key: string]: Serializable }; 2 | 3 | export function convertBigIntToString(obj: Serializable): Serializable { 4 | if (typeof obj === "bigint") { 5 | return obj.toString(); 6 | } 7 | if (obj instanceof Date) { 8 | return obj.toISOString(); 9 | } 10 | if (Array.isArray(obj)) { 11 | return obj.map(convertBigIntToString); 12 | } 13 | if (obj !== null && typeof obj === "object") { 14 | const converted: { [key: string]: Serializable } = {}; 15 | for (const [key, value] of Object.entries(obj)) { 16 | converted[key] = convertBigIntToString(value); 17 | } 18 | return converted; 19 | } 20 | return obj; 21 | } 22 | 23 | export function convertBigIntToNumber(obj: Serializable): Serializable { 24 | if (typeof obj === "bigint") { 25 | if (obj > Number.MAX_SAFE_INTEGER || obj < Number.MIN_SAFE_INTEGER) { 26 | throw new Error(`BigInt value ${obj} is outside safe integer range`); 27 | } 28 | return Number(obj); 29 | } 30 | if (obj instanceof Date) { 31 | return obj.toISOString(); 32 | } 33 | if (Array.isArray(obj)) { 34 | return obj.map(convertBigIntToNumber); 35 | } 36 | if (obj !== null && typeof obj === "object") { 37 | const converted: { [key: string]: Serializable } = {}; 38 | for (const [key, value] of Object.entries(obj)) { 39 | converted[key] = convertBigIntToNumber(value); 40 | } 41 | return converted; 42 | } 43 | return obj; 44 | } 45 | -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "hono/jsx", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@repo/*": ["../../packages/*"] 9 | }, 10 | "esModuleInterop": true, 11 | }, 12 | "include": ["src"] 13 | } -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | AUTH_SECRET="bwHw96+4A6iU6TByCKybJo12IPir0X+Un31tnZClIAA=" 2 | 3 | AUTH_KEYCLOAK_ID="alihan_0khcDSiJq7CvGCnR0EA09MXVIulZBFwg3OG" 4 | AUTH_KEYCLOAK_SECRET="N8MBUX9yKF3Id1yIIG5nMMjrAI7qUV01" 5 | AUTH_KEYCLOAK_ISSUER="https://keycloak.thenoob.gg/realms/noobgg" -------------------------------------------------------------------------------- /apps/web/.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env 35 | .env.local 36 | .env.development.local 37 | .env.test.local 38 | .env.production.local 39 | 40 | # vercel 41 | .vercel 42 | 43 | # typescript 44 | *.tsbuildinfo 45 | next-env.d.ts 46 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /apps/web/app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import LandingHeader from "@/components/mvpblocks/landing-header"; 2 | import "@/styles/globals.css"; 3 | import type { Metadata } from "next"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Gamers'Homepage", 7 | }; 8 | 9 | export default function RootLayout({ 10 | children, 11 | }: Readonly<{ 12 | children: React.ReactNode; 13 | }>) { 14 | return ( 15 |
21 | {/* Radial gradient glows from Globe3D, now global */} 22 |
29 |
36 | {/* Content wrapper to ensure it's above the glows */} 37 |
38 | 39 | 40 |
{children}
41 |
42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/app/(root)/page.tsx: -------------------------------------------------------------------------------- 1 | import LandingHeroSection from "@/components/mvpblocks/landing-hero-section"; 2 | import SparklesLogo from "@/components/mvpblocks/sparkles-logo"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/auth"; // Referring to the auth.ts we just created 2 | export const { GET, POST } = handlers; 3 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/gameranks/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from 'react'; 3 | import { useRouter, useParams } from 'next/navigation'; 4 | import { GameRankForm } from '@/components/gameranks/gamerank-form'; 5 | import { useGameRank } from '@/features/gameranks/api/use-gameranks'; 6 | 7 | export default function EditGameRankPage() { 8 | const router = useRouter(); 9 | const params = useParams(); 10 | const id = Number(params?.id); 11 | const { data: gamerank } = useGameRank(isNaN(id) ? 0 : id); 12 | 13 | if (!gamerank) return
Loading...
; 14 | 15 | return ( 16 |
17 |

Edit Game Rank

18 | router.push('/dashboard/gameranks')} 21 | /> 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/gameranks/new/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from 'react'; 3 | import { useRouter } from 'next/navigation'; 4 | import { GameRankForm } from '@/components/gameranks/gamerank-form'; 5 | 6 | export default function NewGameRankPage() { 7 | const router = useRouter(); 8 | return ( 9 |
10 |

New Game Rank

11 | router.push('/dashboard/gameranks')} /> 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/gameranks/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from 'react'; 3 | import Link from 'next/link'; 4 | import { useRouter } from 'next/navigation'; 5 | import { GameRankTable } from '@/components/gameranks/gamerank-table'; 6 | import { useGameRanks } from '@/features/gameranks/api/use-gameranks'; 7 | 8 | export default function GameRanksPage() { 9 | const router = useRouter(); 10 | const { data } = useGameRanks(); 11 | 12 | if (!data) return
Loading...
; 13 | 14 | return ( 15 |
16 |
17 |

Game Ranks

18 | 19 | New Game Rank 20 | 21 |
22 | router.push(`/dashboard/gameranks/${gr.id}/edit`)} 25 | /> 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/games/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter, useParams } from 'next/navigation'; 3 | import { GameForm } from '@/components/games/game-form'; 4 | import { useGame } from '@/features/games/api/use-games'; 5 | 6 | export default function EditGamePage() { 7 | const router = useRouter(); 8 | const params = useParams(); 9 | const id = Number(params?.id); 10 | const { data: game } = useGame(isNaN(id) ? 0 : id); 11 | 12 | if (!game) return
Loading...
; 13 | 14 | return ( 15 |
16 |

Edit Game

17 | router.push('/dashboard/games')} /> 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/games/new/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from 'next/navigation'; 3 | import { GameForm } from '@/components/games/game-form'; 4 | 5 | export default function NewGamePage() { 6 | const router = useRouter(); 7 | return ( 8 |
9 |

New Game

10 | router.push('/dashboard/games')} /> 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/games/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from 'next/link'; 3 | import { useRouter } from 'next/navigation'; 4 | import { GameTable } from '@/components/games/game-table'; 5 | import { useGames } from '@/features/games/api/use-games'; 6 | 7 | export default function GamesPage() { 8 | const router = useRouter(); 9 | const { data } = useGames(); 10 | 11 | if (!data) return
Loading...
; 12 | 13 | return ( 14 |
15 |
16 |

Games

17 | 18 | New Game 19 | 20 |
21 | router.push(`/dashboard/games/${g.id}/edit`)} /> 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardHeader } from "@/components/dashboard/layout/header"; 2 | import { DashboardSidebar } from "@/components/dashboard/layout/sidebar"; 3 | 4 | import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; 5 | 6 | interface DashboardLayoutProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | export default function DashboardLayout({ children }: DashboardLayoutProps) { 11 | return ( 12 | 13 | 14 | 15 | 16 |
{children}
17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const DashboardPage = () => { 4 | return ( 5 |
DashboardPage
6 | ) 7 | } 8 | 9 | export default DashboardPage -------------------------------------------------------------------------------- /apps/web/app/dashboard/platforms/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter, useParams } from 'next/navigation'; 3 | import { PlatformForm } from '@/components/platforms/platform-form'; 4 | import { usePlatform } from '@/features/platforms/api/use-platforms'; 5 | 6 | export default function EditPlatformPage() { 7 | const router = useRouter(); 8 | const params = useParams(); 9 | const id = Number(params?.id); 10 | const { data: platform } = usePlatform(isNaN(id) ? 0 : id); 11 | 12 | if (!platform) return
Loading...
; 13 | 14 | return ( 15 |
16 |

Edit Platform

17 | router.push('/dashboard/platforms')} 20 | /> 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/platforms/new/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from 'next/navigation'; 3 | import { PlatformForm } from '@/components/platforms/platform-form'; 4 | 5 | export default function NewPlatformPage() { 6 | const router = useRouter(); 7 | return ( 8 |
9 |

New Platform

10 | router.push('/dashboard/platforms')} /> 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/platforms/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from 'next/link'; 3 | import { useRouter } from 'next/navigation'; 4 | import { PlatformTable } from '@/components/platforms/platform-table'; 5 | import { usePlatforms } from '@/features/platforms/api/use-platforms'; 6 | 7 | export default function PlatformsPage() { 8 | const router = useRouter(); 9 | const { data } = usePlatforms(); 10 | 11 | if (!data) return
Loading...
; 12 | 13 | return ( 14 |
15 |
16 |

Platforms

17 | 18 | New Platform 19 | 20 |
21 | router.push(`/dashboard/platforms/${p.id}/edit`)} 24 | /> 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { QueryProvider } from "@/components/query-provider"; 2 | import type { Metadata } from "next"; 3 | import { NextIntlClientProvider } from "next-intl"; 4 | import { getLocale } from "next-intl/server"; 5 | import { Poppins, Exo_2 } from "next/font/google"; 6 | import "@/styles/globals.css"; 7 | import { ThemeProvider } from "next-themes"; 8 | import { SessionProvider } from "next-auth/react"; 9 | 10 | export const metadata: Metadata = { 11 | title: { template: "%s | noob.gg", default: "noob.gg" }, 12 | description: "Discover and explore games on noob.gg gaming platform", 13 | icons: { 14 | icon: [ 15 | { url: "/favicon.ico", sizes: "any" }, 16 | { url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" }, 17 | { url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" }, 18 | ], 19 | apple: [ 20 | { url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }, 21 | ], 22 | other: [ 23 | { 24 | url: "/android-chrome-192x192.png", 25 | sizes: "192x192", 26 | type: "image/png", 27 | }, 28 | { 29 | url: "/android-chrome-512x512.png", 30 | sizes: "512x512", 31 | type: "image/png", 32 | }, 33 | ], 34 | }, 35 | manifest: "/site.webmanifest", 36 | themeColor: "#ffffff", 37 | appleWebApp: { 38 | capable: true, 39 | statusBarStyle: "default", 40 | title: "noob.gg", 41 | }, 42 | }; 43 | 44 | const poppins = Poppins({ 45 | subsets: ["latin"], 46 | weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], 47 | variable: "--font-poppins", 48 | }); 49 | 50 | const exo2 = Exo_2({ 51 | subsets: ["latin"], 52 | weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], 53 | variable: "--font-exo2", 54 | }); 55 | 56 | export default async function RootLayout({ 57 | children, 58 | }: Readonly<{ 59 | children: React.ReactNode; 60 | }>) { 61 | const locale = await getLocale(); 62 | return ( 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {children} 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /apps/web/auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import Keycloak from "next-auth/providers/keycloak"; 3 | 4 | export const { handlers, auth, signIn, signOut } = NextAuth({ 5 | providers: [Keycloak], 6 | callbacks: { 7 | jwt({ token, user, profile }) { 8 | // When user signs in, add the ID to the token 9 | if (user) { 10 | token.id = user.id; 11 | } 12 | // For Keycloak, the user ID might be in the profile.sub 13 | if (profile?.sub) { 14 | token.id = profile.sub; 15 | } 16 | return token; 17 | }, 18 | session({ session, token }) { 19 | // Pass the user ID from token to session 20 | if (token && session.user) { 21 | session.user.id = token.id as string; 22 | } 23 | return session; 24 | }, 25 | }, 26 | trustHost: true, 27 | }); 28 | -------------------------------------------------------------------------------- /apps/web/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": "", 8 | "css": "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 | } 22 | -------------------------------------------------------------------------------- /apps/web/components/LoginButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { signIn } from "next-auth/react"; 3 | import { Button } from "./ui/button"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | export default function LoginButton({ mobile = false }: { mobile?: boolean }) { 7 | return mobile ? ( 8 | 14 | ) : ( 15 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/components/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { signOut } from "next-auth/react"; 3 | import { Button } from "./ui/button"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | export default function LogoutButton({ mobile = false }: { mobile?: boolean }) { 7 | return mobile ? ( 8 | 14 | ) : ( 15 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/components/dashboard/layout/header/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Breadcrumb, 3 | BreadcrumbItem, 4 | BreadcrumbLink, 5 | BreadcrumbList, 6 | BreadcrumbPage, 7 | BreadcrumbSeparator, 8 | } from "@/components/ui/breadcrumb"; 9 | import { Separator } from "@/components/ui/separator"; 10 | import { SidebarTrigger } from "@/components/ui/sidebar"; 11 | export function DashboardHeader() { 12 | return ( 13 |
14 | 15 | 19 | 20 | 21 | 22 | Dashboard 23 | 24 | 25 | 26 | Overview 27 | 28 | 29 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/components/dashboard/layout/sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as React from "react"; 3 | import { 4 | AudioWaveform, 5 | Bell, 6 | Building2, 7 | Command, 8 | GalleryVerticalEnd, 9 | Gamepad2, 10 | Home, 11 | MessageSquare, 12 | Monitor, 13 | Settings, 14 | Users, 15 | } from "lucide-react"; 16 | import { NavMain } from "./nav-main"; 17 | import { TeamSwitcher } from "./team-switcher"; 18 | import { 19 | Sidebar, 20 | SidebarContent, 21 | SidebarFooter, 22 | SidebarHeader, 23 | SidebarRail, 24 | } from "@/components/ui/sidebar"; 25 | import { NavUser } from "./nav-user"; 26 | const data = { 27 | user: { 28 | name: "shadcn", 29 | email: "m@example.com", 30 | avatar: "/avatars/shadcn.jpg", 31 | }, 32 | teams: [ 33 | { 34 | name: "Acme Inc", 35 | logo: GalleryVerticalEnd, 36 | plan: "Enterprise", 37 | }, 38 | { 39 | name: "Acme Corp.", 40 | logo: AudioWaveform, 41 | plan: "Startup", 42 | }, 43 | { 44 | name: "Evil Corp.", 45 | logo: Command, 46 | plan: "Free", 47 | }, 48 | ], 49 | navMain: [ 50 | { 51 | title: "Dashboard", 52 | url: "/dashboard", 53 | icon: Home, 54 | items: [], 55 | }, 56 | { 57 | title: "Lobbies", 58 | url: "/dashboard/lobbies", 59 | icon: MessageSquare, 60 | items: [], 61 | }, 62 | { 63 | title: "Games", 64 | url: "/dashboard/games", 65 | icon: Gamepad2, 66 | items: [], 67 | }, 68 | { 69 | title: "Platforms", 70 | url: "/dashboard/platforms", 71 | icon: Monitor, 72 | items: [], 73 | }, 74 | { 75 | title: "Distributors", 76 | url: "/dashboard/distributors", 77 | icon: Building2, 78 | items: [], 79 | }, 80 | { 81 | title: "Users", 82 | url: "/dashboard/users", 83 | icon: Users, 84 | items: [], 85 | }, 86 | { 87 | title: "Notifications", 88 | url: "/dashboard/notifications", 89 | icon: Bell, 90 | items: [], 91 | }, 92 | { 93 | title: "Settings", 94 | url: "/dashboard/settings", 95 | icon: Settings, 96 | items: [ 97 | { 98 | title: "Sub Item Test", 99 | url: "#", 100 | }, 101 | ], 102 | }, 103 | ], 104 | }; 105 | export function DashboardSidebar({ 106 | ...props 107 | }: React.ComponentProps) { 108 | return ( 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /apps/web/components/dashboard/layout/sidebar/nav-main.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | ChevronRight, 5 | Folder, 6 | Forward, 7 | MoreHorizontal, 8 | type LucideIcon, 9 | } from "lucide-react"; 10 | 11 | import { 12 | Collapsible, 13 | CollapsibleContent, 14 | CollapsibleTrigger, 15 | } from "@/components/ui/collapsible"; 16 | import { 17 | SidebarGroup, 18 | SidebarGroupLabel, 19 | SidebarMenu, 20 | SidebarMenuAction, 21 | SidebarMenuButton, 22 | SidebarMenuItem, 23 | SidebarMenuSub, 24 | SidebarMenuSubButton, 25 | SidebarMenuSubItem, 26 | } from "@/components/ui/sidebar"; 27 | import { 28 | DropdownMenu, 29 | DropdownMenuContent, 30 | DropdownMenuItem, 31 | DropdownMenuTrigger, 32 | } from "@/components/ui/dropdown-menu"; 33 | 34 | export function NavMain({ 35 | items, 36 | }: { 37 | items: { 38 | title: string; 39 | url: string; 40 | icon?: LucideIcon; 41 | isActive?: boolean; 42 | items?: { 43 | title: string; 44 | url: string; 45 | }[]; 46 | }[]; 47 | }) { 48 | return ( 49 | 50 | Platform 51 | 52 | {items.map((item) => { 53 | const { title, url, icon: Icon, isActive, items: subItems } = item; 54 | return item.items?.length === 0 ? ( 55 | 56 | 57 | 58 | {Icon && } 59 | {title} 60 | 61 | 62 | 63 | ) : ( 64 | 70 | 71 | 72 | 73 | {Icon && } 74 | {title} 75 | 76 | 77 | 78 | 79 | 80 | {subItems?.map((subItem) => ( 81 | 82 | 83 | 84 | {subItem.title} 85 | 86 | 87 | 88 | ))} 89 | 90 | 91 | 92 | 93 | ); 94 | })} 95 | 96 | 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /apps/web/components/dashboard/layout/sidebar/nav-user.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | BadgeCheck, 5 | Bell, 6 | ChevronsUpDown, 7 | CreditCard, 8 | LogOut, 9 | Sparkles, 10 | } from "lucide-react"; 11 | 12 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 13 | import { 14 | DropdownMenu, 15 | DropdownMenuContent, 16 | DropdownMenuGroup, 17 | DropdownMenuItem, 18 | DropdownMenuLabel, 19 | DropdownMenuSeparator, 20 | DropdownMenuTrigger, 21 | } from "@/components/ui/dropdown-menu"; 22 | import { 23 | SidebarMenu, 24 | SidebarMenuButton, 25 | SidebarMenuItem, 26 | useSidebar, 27 | } from "@/components/ui/sidebar"; 28 | 29 | export function NavUser({ 30 | user, 31 | }: { 32 | user: { 33 | name: string; 34 | email: string; 35 | avatar: string; 36 | }; 37 | }) { 38 | const { isMobile } = useSidebar(); 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | 49 | 50 | 51 | CN 52 | 53 |
54 | {user.name} 55 | {user.email} 56 |
57 | 58 |
59 |
60 | 66 | 67 |
68 | 69 | 70 | CN 71 | 72 |
73 | {user.name} 74 | {user.email} 75 |
76 |
77 |
78 | 79 | 80 | 81 | 82 | Upgrade to Pro 83 | 84 | 85 | 86 | 87 | 88 | 89 | Account 90 | 91 | 92 | 93 | Billing 94 | 95 | 96 | 97 | Notifications 98 | 99 | 100 | 101 | 102 | 103 | Log out 104 | 105 |
106 |
107 |
108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /apps/web/components/dashboard/layout/sidebar/team-switcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ChevronsUpDown, Plus } from "lucide-react"; 5 | 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuLabel, 11 | DropdownMenuSeparator, 12 | DropdownMenuShortcut, 13 | DropdownMenuTrigger, 14 | } from "@/components/ui/dropdown-menu"; 15 | import { 16 | SidebarMenu, 17 | SidebarMenuButton, 18 | SidebarMenuItem, 19 | useSidebar, 20 | } from "@/components/ui/sidebar"; 21 | 22 | export function TeamSwitcher({ 23 | teams, 24 | }: { 25 | teams: { 26 | name: string; 27 | logo: React.ElementType; 28 | plan: string; 29 | }[]; 30 | }) { 31 | const { isMobile } = useSidebar(); 32 | const [activeTeam, setActiveTeam] = React.useState(teams[0]); 33 | 34 | if (!activeTeam) { 35 | return null; 36 | } 37 | 38 | return ( 39 | 40 | 41 | 42 | 43 | 47 |
48 | 49 |
50 |
51 | {activeTeam.name} 52 | {activeTeam.plan} 53 |
54 | 55 |
56 |
57 | 63 | 64 | Teams 65 | 66 | {teams.map((team, index) => ( 67 | setActiveTeam(team)} 70 | className="gap-2 p-2" 71 | > 72 |
73 | 74 |
75 | {team.name} 76 | ⌘{index + 1} 77 |
78 | ))} 79 | 80 | 81 |
82 | 83 |
84 |
Add team
85 |
86 |
87 |
88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /apps/web/components/gameranks/gamerank-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from 'react'; 4 | import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; 5 | import { Button } from '@/components/ui/button'; 6 | import { useDeleteGameRank } from '@/features/gameranks/api/use-gameranks'; 7 | import type { GameRank } from '@/types/gamerank'; 8 | 9 | export function GameRankTable({ data, onEdit }: { data: GameRank[]; onEdit: (gr: GameRank) => void }) { 10 | const deleteMutation = useDeleteGameRank(); 11 | 12 | const columns: ColumnDef[] = [ 13 | { accessorKey: 'id', header: 'ID' }, 14 | { accessorKey: 'name', header: 'Name' }, 15 | { accessorKey: 'image', header: 'Image' }, 16 | { accessorKey: 'order', header: 'Order' }, 17 | { accessorKey: 'gameId', header: 'Game ID' }, 18 | { 19 | id: 'actions', 20 | header: 'Actions', 21 | cell: ({ row }) => ( 22 |
23 | 26 | 34 |
35 | ), 36 | }, 37 | ]; 38 | 39 | const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }); 40 | 41 | return ( 42 | 43 | 44 | {table.getHeaderGroups().map((hg) => ( 45 | 46 | {hg.headers.map((h) => ( 47 | 50 | ))} 51 | 52 | ))} 53 | 54 | 55 | {table.getRowModel().rows.map((row) => ( 56 | 57 | {row.getVisibleCells().map((cell) => ( 58 | 61 | ))} 62 | 63 | ))} 64 | 65 |
48 | {h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())} 49 |
59 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 60 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /apps/web/components/games/game-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import type { Game } from "@/types/game"; 5 | import Image from "next/image"; 6 | import * as React from "react"; 7 | 8 | interface GameCardProps { 9 | game: Game; 10 | className?: string; 11 | onClick?: () => void; 12 | } 13 | 14 | function GameCard({ game, className, onClick }: GameCardProps) { 15 | const [imageError, setImageError] = React.useState(false); 16 | 17 | return ( 18 |
25 | {/* Glow effect */} 26 |
27 | 28 | {/* Card content */} 29 |
30 | {/* Game logo/image */} 31 |
32 | {game.logo && !imageError ? ( 33 | {`${game.name} setImageError(true)} 38 | width={64} 39 | height={64} 40 | /> 41 | ) : ( 42 | // Fallback placeholder 43 |
44 | {game.name.charAt(0).toUpperCase()} 45 |
46 | )} 47 |
48 | 49 | {/* Game name */} 50 |

51 | {game.name} 52 |

53 | 54 | {/* Game description */} 55 | {game.description && ( 56 |

57 | {game.description} 58 |

59 | )} 60 | 61 | {/* Hover indicator */} 62 |
63 |
64 |
65 |
66 |
67 | ); 68 | } 69 | 70 | GameCard.displayName = "GameCard"; 71 | 72 | export { GameCard }; 73 | -------------------------------------------------------------------------------- /apps/web/components/games/game-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { zodResolver } from '@hookform/resolvers/zod'; 3 | import { useForm } from 'react-hook-form'; 4 | import { z } from 'zod'; 5 | import { Input } from '@/components/ui/input'; 6 | import { Button } from '@/components/ui/button'; 7 | import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'; 8 | import { createGameSchema } from '@repo/shared'; 9 | import { useCreateGame, useUpdateGame } from '@/features/games/api/use-games'; 10 | import type { Game } from '@/types/game'; 11 | 12 | export function GameForm({ 13 | game, 14 | onSuccess, 15 | }: { 16 | game?: Game; 17 | onSuccess?: () => void; 18 | }) { 19 | const createMutation = useCreateGame(); 20 | const updateMutation = useUpdateGame(game?.id ?? 0); 21 | const mutation = game ? updateMutation : createMutation; 22 | 23 | const form = useForm>({ 24 | resolver: zodResolver(createGameSchema), 25 | defaultValues: { 26 | name: game?.name ?? '', 27 | description: game?.description ?? '', 28 | logo: game?.logo ?? '', 29 | }, 30 | }); 31 | 32 | function onSubmit(values: z.infer) { 33 | mutation.mutate(values, { 34 | onSuccess: () => { 35 | form.reset(); 36 | onSuccess?.(); 37 | }, 38 | }); 39 | } 40 | 41 | return ( 42 |
43 | 44 | ( 48 | 49 | Name 50 | 51 | 52 | 53 | 54 | 55 | )} 56 | /> 57 | ( 61 | 62 | Description 63 | 64 | 65 | 66 | 67 | 68 | )} 69 | /> 70 | ( 74 | 75 | Logo 76 | 77 | 78 | 79 | 80 | 81 | )} 82 | /> 83 | 86 | 87 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /apps/web/components/games/game-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; 4 | import { Button } from '@/components/ui/button'; 5 | import { useDeleteGame } from '@/features/games/api/use-games'; 6 | import type { Game } from '@/types/game'; 7 | 8 | export function GameTable({ data, onEdit }: { data: Game[]; onEdit: (g: Game) => void }) { 9 | const deleteMutation = useDeleteGame(); 10 | 11 | const columns: ColumnDef[] = [ 12 | { accessorKey: 'id', header: 'ID' }, 13 | { accessorKey: 'name', header: 'Name' }, 14 | { accessorKey: 'description', header: 'Description' }, 15 | { 16 | id: 'actions', 17 | header: 'Actions', 18 | cell: ({ row }) => ( 19 |
20 | 23 | 31 |
32 | ), 33 | }, 34 | ]; 35 | 36 | const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }); 37 | 38 | return ( 39 | 40 | 41 | {table.getHeaderGroups().map((hg) => ( 42 | 43 | {hg.headers.map((h) => ( 44 | 47 | ))} 48 | 49 | ))} 50 | 51 | 52 | {table.getRowModel().rows.map((row) => ( 53 | 54 | {row.getVisibleCells().map((cell) => ( 55 | 58 | ))} 59 | 60 | ))} 61 | 62 |
45 | {h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())} 46 |
56 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 57 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /apps/web/components/language-switcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useLocale } from "next-intl"; 4 | import { useRouter } from "next/navigation"; 5 | import { Button } from "./ui/button"; 6 | import Image from "next/image"; 7 | import { useState } from "react"; 8 | import { cn } from "@/lib/utils"; 9 | import { AnimatePresence, motion } from "framer-motion"; 10 | 11 | export default function LanguageSwitcher({ 12 | isScrolled, 13 | }: { 14 | isScrolled: boolean; 15 | }) { 16 | const locale = useLocale(); 17 | const router = useRouter(); 18 | const [flagKey, setFlagKey] = useState(locale); 19 | 20 | const switchLanguage = () => { 21 | const newLocale = locale === "tr" ? "en" : "tr"; 22 | 23 | document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000`; 24 | 25 | setFlagKey(newLocale); 26 | 27 | router.refresh(); 28 | }; 29 | 30 | const currentLang = locale === "tr" ? "TR" : "EN"; 31 | const flagSrc = locale === "tr" ? "/flags/tr.svg" : "/flags/en.svg"; 32 | 33 | return ( 34 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /apps/web/components/mvpblocks/landing-hero-section.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { motion } from "framer-motion"; 5 | import StarWarsButton from "../syntax-ui/starwars-button"; 6 | 7 | export default function LandingHeroSection() { 8 | return ( 9 |
13 | {/* Removed the two div elements for radial gradient glows */} 14 |
15 | 20 | 21 | Join 2,000+ gamers who found their perfect squad in under 5 minutes 22 | 23 |

24 | Still Getting Matched With{" "} 25 | Randoms Who Rage Quit? 26 |

27 | 28 |
29 | 30 | 31 | Find Your Squad Now 32 | 33 | 34 |
35 |
36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /apps/web/components/mvpblocks/sparkles-logo.tsx: -------------------------------------------------------------------------------- 1 | import { SparklesCore } from "@/components/ui/sparkles"; 2 | 3 | import Image from "next/image"; 4 | 5 | export default function SparklesLogo() { 6 | return ( 7 |
8 |
9 |
10 | 11 | Pick Your Game. Find Your People. 12 | 13 |
14 | 15 |
16 | Valorant Logo 23 | League of Legends Logo 30 | Fortnite 37 | PlayerUnknown's Battlegrounds 44 | 45 | Counter Strike 2 52 |
53 |
54 | 55 |
56 | 62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /apps/web/components/platforms/platform-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { zodResolver } from '@hookform/resolvers/zod'; 3 | import { useForm } from 'react-hook-form'; 4 | import { z } from 'zod'; 5 | import { Input } from '@/components/ui/input'; 6 | import { Button } from '@/components/ui/button'; 7 | import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'; 8 | import { createPlatformSchema } from '@repo/shared'; 9 | import { useCreatePlatform, useUpdatePlatform } from '@/features/platforms/api/use-platforms'; 10 | import type { Platform } from '@/types/platform'; 11 | 12 | export function PlatformForm({ 13 | platform, 14 | onSuccess, 15 | }: { 16 | platform?: Platform; 17 | onSuccess?: () => void; 18 | }) { 19 | const createMutation = useCreatePlatform(); 20 | const updateMutation = useUpdatePlatform(platform?.id ?? 0); 21 | const mutation = platform ? updateMutation : createMutation; 22 | 23 | const form = useForm>({ 24 | resolver: zodResolver(createPlatformSchema), 25 | defaultValues: { name: platform?.name ?? '' }, 26 | }); 27 | 28 | function onSubmit(values: z.infer) { 29 | mutation.mutate(values, { 30 | onSuccess: () => { 31 | form.reset(); 32 | onSuccess?.(); 33 | }, 34 | }); 35 | } 36 | 37 | return ( 38 |
39 | 40 | ( 44 | 45 | Name 46 | 47 | 48 | 49 | 50 | 51 | )} 52 | /> 53 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /apps/web/components/platforms/platform-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; 4 | import { Button } from '@/components/ui/button'; 5 | import { useDeletePlatform } from '@/features/platforms/api/use-platforms'; 6 | import type { Platform } from '@/types/platform'; 7 | 8 | export function PlatformTable({ data, onEdit }: { data: Platform[]; onEdit: (p: Platform) => void }) { 9 | const deleteMutation = useDeletePlatform(); 10 | 11 | const columns: ColumnDef[] = [ 12 | { accessorKey: 'id', header: 'ID' }, 13 | { accessorKey: 'name', header: 'Name' }, 14 | { 15 | id: 'actions', 16 | header: 'Actions', 17 | cell: ({ row }) => ( 18 |
19 | 22 | 30 |
31 | ), 32 | }, 33 | ]; 34 | 35 | const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }); 36 | 37 | return ( 38 | 39 | 40 | {table.getHeaderGroups().map((hg) => ( 41 | 42 | {hg.headers.map((h) => ( 43 | 46 | ))} 47 | 48 | ))} 49 | 50 | 51 | {table.getRowModel().rows.map((row) => ( 52 | 53 | {row.getVisibleCells().map((cell) => ( 54 | 57 | ))} 58 | 59 | ))} 60 | 61 |
44 | {h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())} 45 |
55 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 56 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /apps/web/components/query-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | import { useState } from 'react'; 4 | 5 | export function QueryProvider({ children }: { children: React.ReactNode }) { 6 | const [client] = useState(() => new QueryClient()); 7 | return {children}; 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/components/syntax-ui/starwars-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState, useRef, useEffect } from "react"; 3 | import { motion, useAnimation } from "framer-motion"; 4 | 5 | interface Star { 6 | id: number; 7 | x: number; 8 | y: number; 9 | size: number; 10 | speed: number; 11 | } 12 | 13 | interface StarWarsButtonProps { 14 | children: React.ReactNode; 15 | className?: string; 16 | } 17 | 18 | const StarWarsButton: React.FC = ({ 19 | children, 20 | className, 21 | }) => { 22 | const buttonRef = useRef(null); 23 | const [stars, setStars] = useState([]); 24 | const controls = useAnimation(); 25 | 26 | useEffect(() => { 27 | const generateStars = () => { 28 | if (buttonRef.current) { 29 | const { width, height } = buttonRef.current.getBoundingClientRect(); 30 | setStars( 31 | Array.from({ length: 50 }, (_, i) => ({ 32 | id: i, 33 | x: Math.random() * width, 34 | y: Math.random() * height, 35 | size: Math.random() * 2 + 1, 36 | speed: Math.random() * 50 + 20, 37 | })) 38 | ); 39 | } 40 | }; 41 | 42 | generateStars(); 43 | window.addEventListener("resize", generateStars); 44 | return () => window.removeEventListener("resize", generateStars); 45 | }, []); 46 | 47 | return ( 48 | 62 | {stars.map((star) => ( 63 | 83 | ))} 84 | {children} 85 | 86 | ); 87 | }; 88 | 89 | export default StarWarsButton; 90 | -------------------------------------------------------------------------------- /apps/web/components/theme-switcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Button } from "./ui/button"; 5 | import { Moon, Sun } from "lucide-react"; 6 | import { useEffect, useState } from "react"; 7 | import { cn } from "@/lib/utils"; 8 | 9 | export default function ThemeSwitcher({ isScrolled }: { isScrolled: boolean }) { 10 | const { resolvedTheme, setTheme } = useTheme(); 11 | const [mounted, setMounted] = useState(false); 12 | 13 | useEffect(() => { 14 | setMounted(true); 15 | }, []); 16 | 17 | const handleThemeSwitch = () => { 18 | const next = resolvedTheme === "dark" ? "light" : "dark"; 19 | setTheme(next); 20 | }; 21 | 22 | if (!mounted) { 23 | return ( 24 | 27 | ); 28 | } 29 | 30 | const isDark = resolvedTheme === "dark"; 31 | const Icon = isDark ? Moon : Sun; 32 | const label = isDark ? "Karanlık" : "Aydınlık"; 33 | 34 | return ( 35 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } -------------------------------------------------------------------------------- /apps/web/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { ChevronRight, MoreHorizontal } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { 8 | return