├── .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 |
5 |
6 | [](https://turbo.build/repo)
7 | [](https://nextjs.org/)
8 | [](https://hono.dev/)
9 | [](https://www.typescriptlang.org/)
10 | [](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 |
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 |
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 | signIn("keycloak", { callbackUrl: "/" })}
11 | >
12 | Sign In
13 |
14 | ) : (
15 | signIn("keycloak", { callbackUrl: "/" })}
19 | >
20 | Sign In
21 |
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 | signOut({ callbackUrl: "/" })}
11 | >
12 | Sign Out
13 |
14 | ) : (
15 | signOut({ callbackUrl: "/" })}
21 | >
22 | Sign Out
23 |
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 |
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 |
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 | onEdit(row.original)}>
24 | Edit
25 |
26 | deleteMutation.mutate(row.original.id)}
30 | disabled={deleteMutation.isPending}
31 | >
32 | Delete
33 |
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 |
48 | {h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())}
49 |
50 | ))}
51 |
52 | ))}
53 |
54 |
55 | {table.getRowModel().rows.map((row) => (
56 |
57 | {row.getVisibleCells().map((cell) => (
58 |
59 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
60 |
61 | ))}
62 |
63 | ))}
64 |
65 |
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 |
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 |
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 |
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 | onEdit(row.original)}>
21 | Edit
22 |
23 | deleteMutation.mutate(row.original.id)}
27 | disabled={deleteMutation.isPending}
28 | >
29 | Delete
30 |
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 |
45 | {h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())}
46 |
47 | ))}
48 |
49 | ))}
50 |
51 |
52 | {table.getRowModel().rows.map((row) => (
53 |
54 | {row.getVisibleCells().map((cell) => (
55 |
56 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
57 |
58 | ))}
59 |
60 | ))}
61 |
62 |
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 |
42 |
43 |
51 |
58 |
59 |
60 |
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 |
23 |
30 |
37 |
44 |
45 |
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 |
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 | onEdit(row.original)}>
20 | Edit
21 |
22 | deleteMutation.mutate(row.original.id)}
26 | disabled={deleteMutation.isPending}
27 | >
28 | Delete
29 |
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 |
44 | {h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())}
45 |
46 | ))}
47 |
48 | ))}
49 |
50 |
51 | {table.getRowModel().rows.map((row) => (
52 |
53 | {row.getVisibleCells().map((cell) => (
54 |
55 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
56 |
57 | ))}
58 |
59 | ))}
60 |
61 |
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 |
25 |
26 |
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 |
43 |
44 |
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
9 | }
10 |
11 | function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25 | return (
26 |
31 | )
32 | }
33 |
34 | function BreadcrumbLink({
35 | asChild,
36 | className,
37 | ...props
38 | }: React.ComponentProps<"a"> & {
39 | asChild?: boolean
40 | }) {
41 | const Comp = asChild ? Slot : "a"
42 |
43 | return (
44 |
49 | )
50 | }
51 |
52 | function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53 | return (
54 |
62 | )
63 | }
64 |
65 | function BreadcrumbSeparator({
66 | children,
67 | className,
68 | ...props
69 | }: React.ComponentProps<"li">) {
70 | return (
71 | svg]:size-3.5", className)}
76 | {...props}
77 | >
78 | {children ?? }
79 |
80 | )
81 | }
82 |
83 | function BreadcrumbEllipsis({
84 | className,
85 | ...props
86 | }: React.ComponentProps<"span">) {
87 | return (
88 |
95 |
96 | More
97 |
98 | )
99 | }
100 |
101 | export {
102 | Breadcrumb,
103 | BreadcrumbList,
104 | BreadcrumbItem,
105 | BreadcrumbLink,
106 | BreadcrumbPage,
107 | BreadcrumbSeparator,
108 | BreadcrumbEllipsis,
109 | }
110 |
--------------------------------------------------------------------------------
/apps/web/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | gaming: "bg-gradient-to-r from-[#6f52f4] to-[#9b87f5] px-6 py-2.5 font-medium text-white transition-all duration-200 hover:shadow-lg"
24 | },
25 | size: {
26 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
27 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
28 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
29 | icon: "size-9",
30 | },
31 | },
32 | defaultVariants: {
33 | variant: "default",
34 | size: "default",
35 | },
36 | }
37 | )
38 |
39 | const Button = React.forwardRef<
40 | HTMLButtonElement,
41 | React.ComponentProps<"button"> &
42 | VariantProps & {
43 | asChild?: boolean
44 | }
45 | >(function Button(
46 | {
47 | className,
48 | variant,
49 | size,
50 | asChild = false,
51 | ...props
52 | },
53 | ref,
54 | ) {
55 | const Comp = asChild ? Slot : "button"
56 |
57 | return (
58 |
65 | )
66 | })
67 |
68 | Button.displayName = "Button"
69 |
70 | export { Button, buttonVariants }
71 |
--------------------------------------------------------------------------------
/apps/web/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | function Collapsible({
6 | ...props
7 | }: React.ComponentProps) {
8 | return
9 | }
10 |
11 | function CollapsibleTrigger({
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
19 | )
20 | }
21 |
22 | function CollapsibleContent({
23 | ...props
24 | }: React.ComponentProps) {
25 | return (
26 |
30 | )
31 | }
32 |
33 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
34 |
--------------------------------------------------------------------------------
/apps/web/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/apps/web/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Label({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/apps/web/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronDown } from "lucide-react"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | export interface SelectProps
7 | extends React.SelectHTMLAttributes {
8 | children: React.ReactNode
9 | }
10 |
11 | const Select = React.forwardRef(
12 | ({ className, children, ...props }, ref) => {
13 | return (
14 |
15 |
23 | {children}
24 |
25 |
26 |
27 | )
28 | }
29 | )
30 | Select.displayName = "Select"
31 |
32 | const SelectOption = React.forwardRef<
33 | HTMLOptionElement,
34 | React.OptionHTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | SelectOption.displayName = "SelectOption"
43 |
44 | export { Select, SelectOption }
--------------------------------------------------------------------------------
/apps/web/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Separator({
9 | className,
10 | orientation = "horizontal",
11 | decorative = true,
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
25 | )
26 | }
27 |
28 | export { Separator }
29 |
--------------------------------------------------------------------------------
/apps/web/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4 | return (
5 |
10 | )
11 | }
12 |
13 | export { Skeleton }
14 |
--------------------------------------------------------------------------------
/apps/web/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
--------------------------------------------------------------------------------
/apps/web/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function TooltipProvider({
9 | delayDuration = 0,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | )
19 | }
20 |
21 | function Tooltip({
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | function TooltipTrigger({
32 | ...props
33 | }: React.ComponentProps) {
34 | return
35 | }
36 |
37 | function TooltipContent({
38 | className,
39 | sideOffset = 0,
40 | children,
41 | ...props
42 | }: React.ComponentProps) {
43 | return (
44 |
45 |
54 | {children}
55 |
56 |
57 |
58 | )
59 | }
60 |
61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
62 |
--------------------------------------------------------------------------------
/apps/web/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/apps/web/features/gameranks/api/actions.ts:
--------------------------------------------------------------------------------
1 | import { createGameRankSchema, updateGameRankSchema } from "@repo/shared";
2 | import type { GameRank, GameRanksResponse } from "@/types/gamerank";
3 |
4 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000";
5 |
6 | export async function getAllGameRanks(): Promise {
7 | try {
8 | const res = await fetch(`${API_BASE_URL}/game-ranks`, {
9 | method: "GET",
10 | headers: { "Content-Type": "application/json" },
11 | cache: "no-store",
12 | });
13 | if (!res.ok) {
14 | throw new Error(`Failed to fetch game ranks: ${res.status}`);
15 | }
16 | return res.json();
17 | } catch (error) {
18 | console.error("Error fetching game ranks:", error);
19 | throw error;
20 | }
21 | }
22 |
23 | export async function getGameRank(id: number): Promise {
24 | try {
25 | const res = await fetch(`${API_BASE_URL}/game-ranks/${id}`, {
26 | method: "GET",
27 | headers: { "Content-Type": "application/json" },
28 | cache: "no-store",
29 | });
30 | if (!res.ok) {
31 | throw new Error(`Failed to fetch game rank: ${res.status}`);
32 | }
33 | return res.json();
34 | } catch (error) {
35 | console.error("Error fetching game rank:", error);
36 | throw error;
37 | }
38 | }
39 |
40 | export async function createGameRank(data: unknown): Promise {
41 | try {
42 | const parsed = createGameRankSchema.parse(data);
43 | const res = await fetch(`${API_BASE_URL}/game-ranks`, {
44 | method: "POST",
45 | headers: { "Content-Type": "application/json" },
46 | body: JSON.stringify(parsed),
47 | });
48 | if (!res.ok) {
49 | throw new Error(`Failed to create game rank: ${res.status}`);
50 | }
51 | return res.json();
52 | } catch (error) {
53 | console.error("Error creating game rank:", error);
54 | throw error;
55 | }
56 | }
57 |
58 | export async function updateGameRank(
59 | id: number,
60 | data: unknown,
61 | ): Promise {
62 | try {
63 | const parsed = updateGameRankSchema.parse(data);
64 | const res = await fetch(`${API_BASE_URL}/game-ranks/${id}`, {
65 | method: "PUT",
66 | headers: { "Content-Type": "application/json" },
67 | body: JSON.stringify(parsed),
68 | });
69 | if (!res.ok) {
70 | throw new Error(`Failed to update game rank: ${res.status}`);
71 | }
72 | return res.json();
73 | } catch (error) {
74 | console.error("Error updating game rank:", error);
75 | throw error;
76 | }
77 | }
78 |
79 | export async function deleteGameRank(id: number): Promise {
80 | try {
81 | const res = await fetch(`${API_BASE_URL}/game-ranks/${id}`, {
82 | method: "DELETE",
83 | });
84 | if (!res.ok) {
85 | throw new Error(`Failed to delete game rank: ${res.status}`);
86 | }
87 | } catch (error) {
88 | console.error("Error deleting game rank:", error);
89 | throw error;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/apps/web/features/gameranks/api/use-gameranks.ts:
--------------------------------------------------------------------------------
1 | import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2 | import {
3 | getAllGameRanks,
4 | getGameRank,
5 | createGameRank,
6 | updateGameRank,
7 | deleteGameRank,
8 | } from './actions';
9 | import type { GameRank } from '@/types/gamerank';
10 |
11 | export function useGameRanks() {
12 | return useQuery({ queryKey: ['gameranks'], queryFn: getAllGameRanks });
13 | }
14 |
15 | export function useGameRank(id: number) {
16 | return useQuery({
17 | queryKey: ['gamerank', id],
18 | queryFn: () => getGameRank(id),
19 | enabled: !!id,
20 | });
21 | }
22 |
23 | export function useCreateGameRank() {
24 | const queryClient = useQueryClient();
25 | return useMutation({
26 | mutationFn: createGameRank,
27 | onSuccess: () => {
28 | queryClient.invalidateQueries({ queryKey: ['gameranks'] });
29 | },
30 | });
31 | }
32 |
33 | export function useUpdateGameRank(id: number) {
34 | const queryClient = useQueryClient();
35 | return useMutation({
36 | mutationFn: (data: Partial) => updateGameRank(id, data),
37 | onSuccess: () => {
38 | queryClient.invalidateQueries({ queryKey: ['gameranks'] });
39 | },
40 | });
41 | }
42 |
43 | export function useDeleteGameRank() {
44 | const queryClient = useQueryClient();
45 | return useMutation({
46 | mutationFn: deleteGameRank,
47 | onSuccess: () => queryClient.invalidateQueries({ queryKey: ['gameranks'] }),
48 | });
49 | }
50 |
--------------------------------------------------------------------------------
/apps/web/features/games/api/actions.ts:
--------------------------------------------------------------------------------
1 | import { createGameSchema, updateGameSchema } from "@repo/shared";
2 | import type { Game, GamesResponse } from "@/types/game";
3 |
4 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000";
5 |
6 | export async function getAllGames(): Promise {
7 | try {
8 | const response = await fetch(`${API_BASE_URL}/games`, {
9 | method: "GET",
10 | headers: {
11 | "Content-Type": "application/json",
12 | },
13 | cache: "no-store", // Always fetch fresh data
14 | });
15 |
16 | if (!response.ok) {
17 | throw new Error(`Failed to fetch games: ${response.status}`);
18 | }
19 |
20 | const games = await response.json();
21 | return games;
22 | } catch (error) {
23 | console.error("Error fetching games:", error);
24 | throw error;
25 | }
26 | }
27 |
28 | export async function getGame(id: number): Promise {
29 | try {
30 | const res = await fetch(`${API_BASE_URL}/games/${id}`, {
31 | method: "GET",
32 | headers: { "Content-Type": "application/json" },
33 | cache: "no-store",
34 | });
35 | if (!res.ok) {
36 | throw new Error(`Failed to fetch game: ${res.status}`);
37 | }
38 | return res.json();
39 | } catch (error) {
40 | console.error("Error fetching game:", error);
41 | throw error;
42 | }
43 | }
44 |
45 | export async function createGame(data: unknown): Promise {
46 | try {
47 | const parsed = createGameSchema.parse(data);
48 | const res = await fetch(`${API_BASE_URL}/games`, {
49 | method: "POST",
50 | headers: { "Content-Type": "application/json" },
51 | body: JSON.stringify(parsed),
52 | });
53 | if (!res.ok) {
54 | throw new Error(`Failed to create game: ${res.status}`);
55 | }
56 | return res.json();
57 | } catch (error) {
58 | console.error("Error creating game:", error);
59 | throw error;
60 | }
61 | }
62 |
63 | export async function updateGame(id: number, data: unknown): Promise {
64 | try {
65 | const parsed = updateGameSchema.parse(data);
66 | const res = await fetch(`${API_BASE_URL}/games/${id}`, {
67 | method: "PUT",
68 | headers: { "Content-Type": "application/json" },
69 | body: JSON.stringify(parsed),
70 | });
71 | if (!res.ok) {
72 | throw new Error(`Failed to update game: ${res.status}`);
73 | }
74 | return res.json();
75 | } catch (error) {
76 | console.error("Error updating game:", error);
77 | throw error;
78 | }
79 | }
80 |
81 | export async function deleteGame(id: number): Promise {
82 | try {
83 | const res = await fetch(`${API_BASE_URL}/games/${id}`, {
84 | method: "DELETE",
85 | });
86 | if (!res.ok) {
87 | throw new Error(`Failed to delete game: ${res.status}`);
88 | }
89 | } catch (error) {
90 | console.error("Error deleting game:", error);
91 | throw error;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/apps/web/features/games/api/use-games.ts:
--------------------------------------------------------------------------------
1 | import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2 | import {
3 | getAllGames,
4 | getGame,
5 | createGame,
6 | updateGame,
7 | deleteGame,
8 | } from './actions';
9 | import type { Game } from '@/types/game';
10 |
11 | export function useGames() {
12 | return useQuery({ queryKey: ['games'], queryFn: getAllGames });
13 | }
14 |
15 | export function useGame(id: number) {
16 | return useQuery({
17 | queryKey: ['game', id],
18 | queryFn: () => getGame(id),
19 | enabled: !!id,
20 | });
21 | }
22 |
23 | export function useCreateGame() {
24 | const queryClient = useQueryClient();
25 | return useMutation({
26 | mutationFn: createGame,
27 | onSuccess: () => queryClient.invalidateQueries({ queryKey: ['games'] }),
28 | });
29 | }
30 |
31 | export function useUpdateGame(id: number) {
32 | const queryClient = useQueryClient();
33 | return useMutation({
34 | mutationFn: (data: Partial) => updateGame(id, data),
35 | onSuccess: () => queryClient.invalidateQueries({ queryKey: ['games'] }),
36 | });
37 | }
38 |
39 | export function useDeleteGame() {
40 | const queryClient = useQueryClient();
41 | return useMutation({
42 | mutationFn: deleteGame,
43 | onSuccess: () => queryClient.invalidateQueries({ queryKey: ['games'] }),
44 | });
45 | }
46 |
--------------------------------------------------------------------------------
/apps/web/features/platforms/api/actions.ts:
--------------------------------------------------------------------------------
1 | import { createPlatformSchema, updatePlatformSchema } from "@repo/shared";
2 | import type { Platform, PlatformsResponse } from "@/types/platform";
3 |
4 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000";
5 |
6 | export async function getAllPlatforms(): Promise {
7 | try {
8 | const res = await fetch(`${API_BASE_URL}/platforms`, {
9 | method: "GET",
10 | headers: { "Content-Type": "application/json" },
11 | cache: "no-store",
12 | });
13 | if (!res.ok) {
14 | throw new Error(`Failed to fetch platforms: ${res.status}`);
15 | }
16 | return res.json();
17 | } catch (error) {
18 | console.error("Error fetching platforms:", error);
19 | throw error;
20 | }
21 | }
22 |
23 | export async function getPlatform(id: number): Promise {
24 | try {
25 | const res = await fetch(`${API_BASE_URL}/platforms/${id}`, {
26 | method: "GET",
27 | headers: { "Content-Type": "application/json" },
28 | cache: "no-store",
29 | });
30 | if (!res.ok) {
31 | throw new Error(`Failed to fetch platform: ${res.status}`);
32 | }
33 | return res.json();
34 | } catch (error) {
35 | console.error("Error fetching platform:", error);
36 | throw error;
37 | }
38 | }
39 |
40 | export async function createPlatform(data: unknown): Promise {
41 | try {
42 | const parsed = createPlatformSchema.parse(data);
43 | const res = await fetch(`${API_BASE_URL}/platforms`, {
44 | method: "POST",
45 | headers: { "Content-Type": "application/json" },
46 | body: JSON.stringify(parsed),
47 | });
48 | if (!res.ok) {
49 | throw new Error(`Failed to create platform: ${res.status}`);
50 | }
51 | return res.json();
52 | } catch (error) {
53 | console.error("Error creating platform:", error);
54 | throw error;
55 | }
56 | }
57 |
58 | export async function updatePlatform(
59 | id: number,
60 | data: unknown,
61 | ): Promise {
62 | try {
63 | const parsed = updatePlatformSchema.parse(data);
64 | const res = await fetch(`${API_BASE_URL}/platforms/${id}`, {
65 | method: "PUT",
66 | headers: { "Content-Type": "application/json" },
67 | body: JSON.stringify(parsed),
68 | });
69 | if (!res.ok) {
70 | throw new Error(`Failed to update platform: ${res.status}`);
71 | }
72 | return res.json();
73 | } catch (error) {
74 | console.error("Error updating platform:", error);
75 | throw error;
76 | }
77 | }
78 |
79 | export async function deletePlatform(id: number): Promise {
80 | try {
81 | const res = await fetch(`${API_BASE_URL}/platforms/${id}`, {
82 | method: "DELETE",
83 | });
84 | if (!res.ok) {
85 | throw new Error(`Failed to delete platform: ${res.status}`);
86 | }
87 | } catch (error) {
88 | console.error("Error deleting platform:", error);
89 | throw error;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/apps/web/features/platforms/api/use-platforms.ts:
--------------------------------------------------------------------------------
1 | import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2 | import {
3 | getAllPlatforms,
4 | getPlatform,
5 | createPlatform,
6 | updatePlatform,
7 | deletePlatform,
8 | } from './actions';
9 | import type { Platform } from '@/types/platform';
10 |
11 | export function usePlatforms() {
12 | return useQuery({ queryKey: ['platforms'], queryFn: getAllPlatforms });
13 | }
14 |
15 | export function usePlatform(id: number) {
16 | return useQuery({
17 | queryKey: ['platform', id],
18 | queryFn: () => getPlatform(id),
19 | enabled: !!id,
20 | });
21 | }
22 |
23 | export function useCreatePlatform() {
24 | const queryClient = useQueryClient();
25 | return useMutation({
26 | mutationFn: createPlatform,
27 | onSuccess: () => queryClient.invalidateQueries({ queryKey: ['platforms'] }),
28 | });
29 | }
30 |
31 | export function useUpdatePlatform(id: number) {
32 | const queryClient = useQueryClient();
33 | return useMutation({
34 | mutationFn: (data: Partial) => updatePlatform(id, data),
35 | onSuccess: () => queryClient.invalidateQueries({ queryKey: ['platforms'] }),
36 | });
37 | }
38 |
39 | export function useDeletePlatform() {
40 | const queryClient = useQueryClient();
41 | return useMutation({
42 | mutationFn: deletePlatform,
43 | onSuccess: () => queryClient.invalidateQueries({ queryKey: ['platforms'] }),
44 | });
45 | }
46 |
--------------------------------------------------------------------------------
/apps/web/features/user-profiles/api/actions.ts:
--------------------------------------------------------------------------------
1 | import type { UserProfile, UserProfileResponse } from "@/types/user-profile";
2 |
3 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
4 |
5 | export async function getUserProfile(id: string): Promise {
6 | try {
7 | const res = await fetch(`${API_BASE_URL}/user-profiles/${id}`, {
8 | method: "GET",
9 | headers: { "Content-Type": "application/json" },
10 | cache: "no-store",
11 | });
12 | if (!res.ok) {
13 | throw new Error(`Failed to fetch user profile: ${res.status}`);
14 | }
15 | return res.json();
16 | } catch (error) {
17 | console.error("Error fetching user profile:", error);
18 | throw error;
19 | }
20 | }
21 |
22 | export async function getUserProfileByUsername(username: string): Promise {
23 | try {
24 | const res = await fetch(`${API_BASE_URL}/user-profiles/username/${username}`, {
25 | method: "GET",
26 | headers: { "Content-Type": "application/json" },
27 | cache: "no-store",
28 | });
29 | if (!res.ok) {
30 | throw new Error(`Failed to fetch user profile: ${res.status}`);
31 | }
32 | return res.json();
33 | } catch (error) {
34 | console.error("Error fetching user profile by username:", error);
35 | throw error;
36 | }
37 | }
38 |
39 | export async function getAllUserProfiles(): Promise {
40 | try {
41 | const res = await fetch(`${API_BASE_URL}/user-profiles`, {
42 | method: "GET",
43 | headers: { "Content-Type": "application/json" },
44 | cache: "no-store",
45 | });
46 | if (!res.ok) {
47 | throw new Error(`Failed to fetch user profiles: ${res.status}`);
48 | }
49 | return res.json();
50 | } catch (error) {
51 | console.error("Error fetching user profiles:", error);
52 | throw error;
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/apps/web/features/user-profiles/api/use-user-profiles.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 | import {
3 | getUserProfile,
4 | getUserProfileByUsername,
5 | getAllUserProfiles,
6 | } from './actions';
7 |
8 | export function useUserProfiles() {
9 | return useQuery({
10 | queryKey: ['user-profiles'],
11 | queryFn: getAllUserProfiles
12 | });
13 | }
14 |
15 | export function useUserProfile(id: string) {
16 | return useQuery({
17 | queryKey: ['user-profile', id],
18 | queryFn: () => getUserProfile(id),
19 | enabled: !!id,
20 | });
21 | }
22 |
23 | export function useUserProfileByUsername(username: string) {
24 | return useQuery({
25 | queryKey: ['user-profile', 'username', username],
26 | queryFn: () => getUserProfileByUsername(username),
27 | enabled: !!username,
28 | });
29 | }
--------------------------------------------------------------------------------
/apps/web/hooks/use-mobile.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/i18n/request.ts:
--------------------------------------------------------------------------------
1 | import { getRequestConfig } from "next-intl/server";
2 | import { cookies } from "next/headers";
3 |
4 | export default getRequestConfig(async () => {
5 | const cookieStore = await cookies();
6 | const locale = cookieStore.get("NEXT_LOCALE")?.value || "tr";
7 |
8 | return {
9 | locale,
10 | messages: (await import(`../messages/${locale}.json`)).default,
11 | };
12 | });
13 |
--------------------------------------------------------------------------------
/apps/web/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
8 | /**
9 | * Get the user's locale from browser settings or fallback to default
10 | */
11 | export function getUserLocale(): string {
12 | if (typeof window !== 'undefined') {
13 | return navigator.language || 'en-US';
14 | }
15 | return 'en-US';
16 | }
17 |
18 | /**
19 | * Format a date string as a "time ago" string with locale support
20 | */
21 | export function formatTimeAgo(dateString: string, locale?: string): string {
22 | const date = new Date(dateString);
23 | const now = new Date();
24 | const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
25 |
26 | const userLocale = locale || getUserLocale();
27 |
28 | if (diffInSeconds < 60) return 'just now';
29 | if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
30 | if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
31 | if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`;
32 |
33 | return date.toLocaleDateString(userLocale, {
34 | month: 'short',
35 | day: 'numeric',
36 | year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
37 | });
38 | }
39 |
40 | /**
41 | * Format a region string by replacing underscores with spaces and capitalizing words
42 | * @param region - The region string to format (e.g., "north_america")
43 | * @returns Formatted region string (e.g., "North America")
44 | */
45 | export function formatRegion(region: string): string {
46 | return region.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
47 | }
48 |
49 | /**
50 | * Get the color class for status indicator based on current status
51 | * @param status - The current status (online, afk, in-game, offline)
52 | * @returns Color class string for the status indicator
53 | */
54 | export function getStatusIndicatorClass(status: string): string {
55 | switch (status) {
56 | case 'online':
57 | return 'bg-green-500';
58 | case 'afk':
59 | return 'bg-yellow-500';
60 | case 'in-game':
61 | return 'bg-blue-500';
62 | case 'offline':
63 | default:
64 | return 'bg-gray-500';
65 | }
66 | }
67 |
68 | /**
69 | * Format numbers with locale-aware formatting (for stats, counts etc.)
70 | * @param count - The number to format
71 | * @param locale - Optional locale, falls back to user locale
72 | * @returns Formatted number string
73 | */
74 | export function formatNumber(count: number, locale?: string): string {
75 | const userLocale = locale || getUserLocale();
76 | return count.toLocaleString(userLocale);
77 | }
78 |
79 | /**
80 | * Format a full date with locale support
81 | * @param dateString - ISO date string
82 | * @param locale - Optional locale, falls back to user locale
83 | * @param options - Intl.DateTimeFormatOptions for custom formatting
84 | * @returns Formatted date string
85 | */
86 | export function formatDate(
87 | dateString: string,
88 | locale?: string,
89 | options?: Intl.DateTimeFormatOptions
90 | ): string {
91 | const userLocale = locale || getUserLocale();
92 | const defaultOptions: Intl.DateTimeFormatOptions = {
93 | year: 'numeric',
94 | month: 'long',
95 | day: 'numeric'
96 | };
97 |
98 | return new Date(dateString).toLocaleDateString(userLocale, options || defaultOptions);
99 | }
100 |
101 | /**
102 | * Format date and time with locale support
103 | * @param dateString - ISO date string
104 | * @param locale - Optional locale, falls back to user locale
105 | * @returns Formatted date and time string
106 | */
107 | export function formatDateTime(dateString: string, locale?: string): string {
108 | const userLocale = locale || getUserLocale();
109 | return new Date(dateString).toLocaleDateString(userLocale, {
110 | year: 'numeric',
111 | month: 'short',
112 | day: 'numeric',
113 | hour: '2-digit',
114 | minute: '2-digit'
115 | });
116 | }
117 |
--------------------------------------------------------------------------------
/apps/web/messages/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "example": "example"
3 | }
4 |
--------------------------------------------------------------------------------
/apps/web/messages/tr.json:
--------------------------------------------------------------------------------
1 | {
2 | "example": "Örnek"
3 | }
4 |
--------------------------------------------------------------------------------
/apps/web/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { auth } from "./auth";
3 |
4 | const protectedRoutes = ["/dashboard"];
5 |
6 | export default auth((req) => {
7 | const locale = req.cookies.get("NEXT_LOCALE")?.value || "tr";
8 | const { pathname } = req.nextUrl;
9 |
10 | if (protectedRoutes.some((route) => pathname.startsWith(route))) {
11 | if (!req.auth && !req.nextUrl.pathname.startsWith("/login")) {
12 | const newUrl = new URL("/login", req.nextUrl);
13 | const redirectResponse = NextResponse.redirect(newUrl);
14 | redirectResponse.cookies.set("NEXT_LOCALE", locale, {
15 | maxAge: 60 * 60 * 24 * 365,
16 | path: "/",
17 | });
18 |
19 | return redirectResponse;
20 | }
21 | }
22 |
23 | const response = NextResponse.next();
24 | if (!req.cookies.get("NEXT_LOCALE")) {
25 | response.cookies.set("NEXT_LOCALE", locale, {
26 | maxAge: 60 * 60 * 24 * 365,
27 | path: "/",
28 | });
29 | }
30 |
31 | return response;
32 | });
33 |
34 | export const config = {
35 | matcher: [
36 | "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
37 | ],
38 | };
39 |
--------------------------------------------------------------------------------
/apps/web/next.config.ts:
--------------------------------------------------------------------------------
1 | import { NextConfig } from "next";
2 | import createNextIntlPlugin from "next-intl/plugin";
3 |
4 | const nextConfig: NextConfig = {
5 | images: {
6 | remotePatterns: [
7 | {
8 | protocol: 'https',
9 | hostname: 'blocks.mvp-subha.me',
10 | pathname: '/assets/**',
11 | },
12 | ],
13 | },
14 | };
15 |
16 | const withNextIntl = createNextIntlPlugin();
17 | export default withNextIntl(nextConfig);
18 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack -p 3001",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@hookform/resolvers": "^5.0.1",
13 | "@radix-ui/react-avatar": "^1.1.10",
14 | "@radix-ui/react-collapsible": "^1.1.11",
15 | "@radix-ui/react-dialog": "^1.1.14",
16 | "@radix-ui/react-dropdown-menu": "^2.1.15",
17 | "@radix-ui/react-label": "^2.1.7",
18 | "@radix-ui/react-separator": "^1.1.7",
19 | "@radix-ui/react-slot": "^1.2.3",
20 | "@radix-ui/react-tabs": "^1.1.8",
21 | "@radix-ui/react-tooltip": "^1.2.7",
22 | "@tanstack/react-query": "^5.80.2",
23 | "@tanstack/react-table": "^8.21.3",
24 | "@tsparticles/engine": "^3.8.1",
25 | "@tsparticles/react": "^3.0.0",
26 | "@tsparticles/slim": "^3.8.1",
27 | "class-variance-authority": "^0.7.1",
28 | "clsx": "^2.1.1",
29 | "lucide-react": "^0.513.0",
30 | "motion": "^12.16.0",
31 | "next": "15.3.3",
32 | "next-auth": "^5.0.0-beta.28",
33 | "next-intl": "^4.1.0",
34 | "next-themes": "^0.4.6",
35 | "react": "^19.1.0",
36 | "react-dom": "^19.0.0",
37 | "react-hook-form": "^7.57.0",
38 | "tailwind-merge": "^3.3.0",
39 | "zod": "^3.25.49"
40 | },
41 | "devDependencies": {
42 | "@eslint/eslintrc": "^3",
43 | "@tailwindcss/postcss": "^4",
44 | "@types/node": "^22",
45 | "@types/react": "^19",
46 | "@types/react-dom": "^19",
47 | "eslint": "^9",
48 | "eslint-config-next": "15.3.3",
49 | "tailwindcss": "^4",
50 | "tw-animate-css": "^1.3.3",
51 | "typescript": "^5"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ["@tailwindcss/postcss"],
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/apps/web/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/apps/web/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/apps/web/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/apps/web/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/apps/web/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/apps/web/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/apps/web/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/apps/web/public/favicon-16x16.png
--------------------------------------------------------------------------------
/apps/web/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/apps/web/public/favicon-32x32.png
--------------------------------------------------------------------------------
/apps/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/apps/web/public/favicon.ico
--------------------------------------------------------------------------------
/apps/web/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/flags/en.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/flags/tr.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/fortnite-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
9 |
12 |
16 |
22 |
27 |
29 |
30 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/apps/web/public/logos/pubg-logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/apps/web/public/logos/pubg-logo.webp
--------------------------------------------------------------------------------
/apps/web/public/logos/valorant-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/apps/web/public/logos/valorant-logo.png
--------------------------------------------------------------------------------
/apps/web/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/noobgg-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/apps/web/public/noobgg-logo.png
--------------------------------------------------------------------------------
/apps/web/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/apps/web/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/apps/web/types/game.ts:
--------------------------------------------------------------------------------
1 | export type Game = {
2 | id: number;
3 | name: string;
4 | description: string | null;
5 | logo: string | null;
6 | };
7 |
8 | export type GamesResponse = Game[];
9 |
--------------------------------------------------------------------------------
/apps/web/types/gamerank.ts:
--------------------------------------------------------------------------------
1 | export type GameRank = {
2 | id: number;
3 | name: string;
4 | image: string;
5 | order: number;
6 | gameId: number;
7 | };
8 |
9 | export type GameRanksResponse = GameRank[];
10 |
--------------------------------------------------------------------------------
/apps/web/types/platform.ts:
--------------------------------------------------------------------------------
1 | export type Platform = {
2 | id: number;
3 | name: string;
4 | };
5 |
6 | export type PlatformsResponse = Platform[];
7 |
--------------------------------------------------------------------------------
/apps/web/types/user-profile.ts:
--------------------------------------------------------------------------------
1 | export type UserProfile = {
2 | id: string;
3 | userKeycloakId: string;
4 | createdAt: string;
5 | updatedAt: string | null;
6 | deletedAt: string | null;
7 | birthDate: string | null;
8 | userName: string;
9 | firstName: string | null;
10 | lastName: string | null;
11 | profileImageUrl: string | null;
12 | bannerImageUrl: string | null;
13 | bio: string | null;
14 | website: string | null;
15 | gender: 'male' | 'female' | 'unknown';
16 | regionType: 'north_america' | 'south_america' | 'europe' | 'asia' | 'oceania' | 'middle_east' | 'africa' | 'russia_cis' | 'unknown';
17 | lastOnline: string;
18 | rowVersion: string;
19 | // Gaming specific fields
20 | nickname?: string | null;
21 | club?: string | null;
22 | tagline?: string | null;
23 | currentStatus?: 'online' | 'afk' | 'offline' | 'in-game';
24 | gamerType?: string | null;
25 | professionalBackground?: string | null;
26 | };
27 |
28 | export type UserProfileResponse = UserProfile[];
29 |
30 | // Profile page specific types
31 | export type ProfileStats = {
32 | posts: number;
33 | followers: number;
34 | following: number;
35 | };
36 |
37 | // Gaming specific types
38 | export type GameStats = {
39 | winRate: number;
40 | totalGames: number;
41 | rank: string;
42 | rating: number;
43 | hoursPlayed: number;
44 | };
45 |
46 | export type FavoriteGame = {
47 | id: string;
48 | name: string;
49 | iconUrl: string | null;
50 | stats: GameStats;
51 | platform: string;
52 | };
53 |
54 | export type PCHardware = {
55 | id: string;
56 | component: 'cpu' | 'gpu' | 'ram' | 'motherboard' | 'storage' | 'psu' | 'case' | 'monitor' | 'keyboard' | 'mouse' | 'headset';
57 | brand: string;
58 | model: string;
59 | imageUrl?: string | null;
60 | };
61 |
62 | export type GamerExperience = {
63 | id: string;
64 | game: string;
65 | platform: string;
66 | rank: string;
67 | experience: 'beginner' | 'intermediate' | 'advanced' | 'professional';
68 | startDate: string;
69 | achievements: string[];
70 | };
71 |
72 | export type ConnectedPlatform = {
73 | id: string;
74 | platform: 'steam' | 'epic' | 'origin' | 'uplay' | 'battlenet' | 'xbox' | 'playstation' | 'nintendo';
75 | username: string;
76 | profileUrl: string;
77 | verified: boolean;
78 | };
79 |
80 | export type GameReview = {
81 | id: string;
82 | gameId: string;
83 | gameName: string;
84 | gameImageUrl: string | null;
85 | rating: number; // 1-5 stars
86 | reviewText: string;
87 | playedHours: number;
88 | platform: string;
89 | createdAt: string;
90 | likes: number;
91 | helpful: number;
92 | };
93 |
94 | export type SocialLink = {
95 | platform: 'facebook' | 'twitter' | 'instagram' | 'youtube' | 'discord' | 'twitch';
96 | url: string;
97 | };
98 |
99 | export type Friend = {
100 | id: string;
101 | userName: string;
102 | profileImageUrl: string | null;
103 | isOnline: boolean;
104 | currentGame?: string | null;
105 | status?: 'online' | 'afk' | 'offline' | 'in-game';
106 | };
107 |
108 | export type Group = {
109 | id: string;
110 | name: string;
111 | iconUrl: string | null;
112 | memberCount: number;
113 | lastActivity?: string | null;
114 | category?: 'gaming' | 'esports' | 'streaming' | 'community';
115 | isOfficial?: boolean;
116 | };
117 |
118 | export type Post = {
119 | id: string;
120 | userId: string;
121 | userName: string;
122 | userProfileImageUrl: string | null;
123 | content: string;
124 | images: string[];
125 | createdAt: string;
126 | likes: number;
127 | comments: number;
128 | shares: number;
129 | gameTag?: string | null;
130 | postType?: 'achievement' | 'screenshot' | 'review' | 'general';
131 | };
132 |
133 | export type Photo = {
134 | id: string;
135 | url: string;
136 | caption: string | null;
137 | createdAt: string;
138 | category?: 'setup' | 'gameplay' | 'achievement' | 'team' | 'general';
139 | };
140 |
141 | export type Badge = {
142 | id: string;
143 | name: string;
144 | description: string;
145 | iconUrl: string | null;
146 | unlockedAt: string;
147 | rarity: 'common' | 'rare' | 'epic' | 'legendary';
148 | };
149 |
150 | export type Quest = {
151 | id: string;
152 | title: string;
153 | description: string;
154 | progress: number; // 0-100
155 | maxProgress: number;
156 | status: 'completed' | 'in_progress' | 'locked';
157 | reward: string;
158 | expiresAt?: string | null;
159 | };
160 |
161 | export type ProfileTabType = 'about' | 'professional' | 'gamer-experience' | 'timeline' | 'media' | 'friends' | 'reviews';
--------------------------------------------------------------------------------
/docker.md:
--------------------------------------------------------------------------------
1 | # 🐳 PostgreSQL Docker Konteyner Kurulum Adımları
2 |
3 | Bu dokümanda, PostgreSQL veritabanını Docker konteyner olarak nasıl kuracağımızı adım adım açıklayacağız.
4 |
5 | ## 1️⃣ PostgreSQL Docker İmajının İndirilmesi
6 |
7 | İlk adım olarak PostgreSQL'in Alpine Linux tabanlı hafif versiyonunu indiriyoruz:
8 |
9 | ```bash
10 | docker pull postgres:16.9-alpine3.22
11 | ```
12 |
13 | > 💡 **Not:** Bu komut, Docker Hub'dan PostgreSQL'in 16.9 versiyonunu Alpine Linux 3.22 tabanlı imajını indirir. Alpine Linux tabanlı imajlar, boyut olarak daha küçük ve daha güvenlidir.
14 |
15 | ## 2️⃣ PostgreSQL Konteynerinin Oluşturulması ve Çalıştırılması
16 |
17 | İndirilen imajı kullanarak yeni bir konteyner oluşturup çalıştırmak için aşağıdaki komutu kullanıyoruz:
18 |
19 | ```bash
20 | docker run -p 1453:5432 --name noobgg-postgres -e POSTGRES_PASSWORD=123noobgg123++ -d postgres:16.9-alpine3.22
21 | ```
22 |
23 | ### 🔧 Komut Parametrelerinin Açıklaması:
24 |
25 | | Parametre | Açıklama |
26 | |-----------|----------|
27 | | `-p 1453:5432` | Port yönlendirmesi. Host makinedeki 1453 portunu, konteynerin içindeki PostgreSQL'in varsayılan portu olan 5432'ye yönlendirir. |
28 | | `--name noobgg-postgres` | Konteynere verilen isim. Bu isim ile konteyneri daha sonra kolayca yönetebiliriz. |
29 | | `-e POSTGRES_PASSWORD=123noobgg123++` | PostgreSQL root kullanıcısının (postgres) şifresini belirler. |
30 | | `-d` | Konteyneri arka planda (detached mode) çalıştırır. |
31 | | `postgres:16.9-alpine3.22` | Kullanılacak Docker imajının adı ve versiyonu. |
32 |
33 | ## 🔌 Bağlantı Bilgileri
34 |
35 | PostgreSQL veritabanına bağlanmak için aşağıdaki bilgileri kullanabilirsiniz:
36 |
37 | | Parametre | Değer |
38 | |-----------|-------|
39 | | Host | localhost |
40 | | Port | 1453 |
41 | | Kullanıcı Adı | postgres |
42 | | Şifre | 123noobgg123++ |
43 | | Varsayılan Veritabanı | postgres |
44 |
45 | ## 📝 Önemli Docker Komutları
46 |
47 | Konteyner yönetimi için kullanabileceğiniz bazı faydalı komutlar:
48 |
49 | ```bash
50 | # Çalışan konteynerleri listeler
51 | docker ps
52 |
53 | # Konteyneri durdurur
54 | docker stop noobgg-postgres
55 |
56 | # Konteyneri başlatır
57 | docker start noobgg-postgres
58 |
59 | # Konteyner loglarını gösterir
60 | docker logs noobgg-postgres
61 | ```
62 |
63 | ## 🔍 Ek Faydalı Komutlar
64 |
65 | ```bash
66 | # Konteynerin detaylı bilgilerini gösterir
67 | docker inspect noobgg-postgres
68 |
69 | # Konteynerin kaynak kullanımını gösterir
70 | docker stats noobgg-postgres
71 |
72 | # Konteyneri yeniden başlatır
73 | docker restart noobgg-postgres
74 |
75 | # Konteyneri siler (dikkatli kullanın!)
76 | docker rm -f noobgg-postgres
77 | ```
78 |
79 | > ⚠️ **Önemli Not:** Konteyneri silmeden önce veritabanı yedeklerinizi aldığınızdan emin olun!
80 |
81 | ---
82 |
83 |
84 | noob.gg PostgreSQL Docker Kurulum Rehberi
85 |
86 |
--------------------------------------------------------------------------------
/docs/Profile UI Corrections.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/docs/Profile UI Corrections.png
--------------------------------------------------------------------------------
/docs/Screenshot 2025-06-02 204809.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/docs/Screenshot 2025-06-02 204809.png
--------------------------------------------------------------------------------
/docs/Vikinger Profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/docs/Vikinger Profile.png
--------------------------------------------------------------------------------
/docs/globe.md:
--------------------------------------------------------------------------------
1 | https://globe.gl/https://globe.gl/
2 | http://globe.gl/example/hexed-polygons/
3 | https://www.redblobgames.com/
--------------------------------------------------------------------------------
/docs/noobgg-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/docs/noobgg-logo.png
--------------------------------------------------------------------------------
/docs/search-lobby-mock-up-filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/altudev/noobgg/50787d8b17b95ed4bbc69f06cf5434503b738438/docs/search-lobby-mock-up-filled.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "noobgg",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo run build",
6 | "dev": "turbo run dev",
7 | "lint": "turbo run lint",
8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"",
9 | "check-types": "turbo run check-types"
10 | },
11 | "devDependencies": {
12 | "prettier": "^3.5.3",
13 | "turbo": "^2.5.4",
14 | "typescript": "5.8.3"
15 | },
16 | "engines": {
17 | "node": ">=18"
18 | },
19 | "packageManager": "bun@1.2.15",
20 | "workspaces": [
21 | "apps/*",
22 | "packages/*"
23 | ],
24 | "dependencies": {
25 | "@hono/swagger-ui": "^0.5.1",
26 | "@hono/zod-openapi": "^0.19.8",
27 | "swagger-ui-express": "^5.0.1",
28 | "yamljs": "^0.3.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/eslint-config/README.md:
--------------------------------------------------------------------------------
1 | # `@turbo/eslint-config`
2 |
3 | Collection of internal eslint configurations.
4 |
--------------------------------------------------------------------------------
/packages/eslint-config/base.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import eslintConfigPrettier from "eslint-config-prettier";
3 | import turboPlugin from "eslint-plugin-turbo";
4 | import tseslint from "typescript-eslint";
5 | import onlyWarn from "eslint-plugin-only-warn";
6 |
7 | /**
8 | * A shared ESLint configuration for the repository.
9 | *
10 | * @type {import("eslint").Linter.Config[]}
11 | * */
12 | export const config = [
13 | js.configs.recommended,
14 | eslintConfigPrettier,
15 | ...tseslint.configs.recommended,
16 | {
17 | plugins: {
18 | turbo: turboPlugin,
19 | },
20 | rules: {
21 | "turbo/no-undeclared-env-vars": "warn",
22 | },
23 | },
24 | {
25 | plugins: {
26 | onlyWarn,
27 | },
28 | },
29 | {
30 | ignores: ["dist/**"],
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/packages/eslint-config/next.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import eslintConfigPrettier from "eslint-config-prettier";
3 | import tseslint from "typescript-eslint";
4 | import pluginReactHooks from "eslint-plugin-react-hooks";
5 | import pluginReact from "eslint-plugin-react";
6 | import globals from "globals";
7 | import pluginNext from "@next/eslint-plugin-next";
8 | import { config as baseConfig } from "./base.js";
9 |
10 | /**
11 | * A custom ESLint configuration for libraries that use Next.js.
12 | *
13 | * @type {import("eslint").Linter.Config[]}
14 | * */
15 | export const nextJsConfig = [
16 | ...baseConfig,
17 | js.configs.recommended,
18 | eslintConfigPrettier,
19 | ...tseslint.configs.recommended,
20 | {
21 | ...pluginReact.configs.flat.recommended,
22 | languageOptions: {
23 | ...pluginReact.configs.flat.recommended.languageOptions,
24 | globals: {
25 | ...globals.serviceworker,
26 | },
27 | },
28 | },
29 | {
30 | plugins: {
31 | "@next/next": pluginNext,
32 | },
33 | rules: {
34 | ...pluginNext.configs.recommended.rules,
35 | ...pluginNext.configs["core-web-vitals"].rules,
36 | },
37 | },
38 | {
39 | plugins: {
40 | "react-hooks": pluginReactHooks,
41 | },
42 | settings: { react: { version: "detect" } },
43 | rules: {
44 | ...pluginReactHooks.configs.recommended.rules,
45 | // React scope no longer necessary with new JSX transform.
46 | "react/react-in-jsx-scope": "off",
47 | },
48 | },
49 | ];
50 |
--------------------------------------------------------------------------------
/packages/eslint-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/eslint-config",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "private": true,
6 | "exports": {
7 | "./base": "./base.js",
8 | "./next-js": "./next.js",
9 | "./react-internal": "./react-internal.js"
10 | },
11 | "devDependencies": {
12 | "@eslint/js": "^9.28.0",
13 | "@next/eslint-plugin-next": "^15.3.0",
14 | "eslint": "^9.28.0",
15 | "eslint-config-prettier": "^10.1.1",
16 | "eslint-plugin-only-warn": "^1.1.0",
17 | "eslint-plugin-react": "^7.37.4",
18 | "eslint-plugin-react-hooks": "^5.2.0",
19 | "eslint-plugin-turbo": "^2.5.0",
20 | "globals": "^16.2.0",
21 | "typescript": "^5.8.3",
22 | "typescript-eslint": "^8.33.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/eslint-config/react-internal.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import eslintConfigPrettier from "eslint-config-prettier";
3 | import tseslint from "typescript-eslint";
4 | import pluginReactHooks from "eslint-plugin-react-hooks";
5 | import pluginReact from "eslint-plugin-react";
6 | import globals from "globals";
7 | import { config as baseConfig } from "./base.js";
8 |
9 | /**
10 | * A custom ESLint configuration for libraries that use React.
11 | *
12 | * @type {import("eslint").Linter.Config[]} */
13 | export const config = [
14 | ...baseConfig,
15 | js.configs.recommended,
16 | eslintConfigPrettier,
17 | ...tseslint.configs.recommended,
18 | pluginReact.configs.flat.recommended,
19 | {
20 | languageOptions: {
21 | ...pluginReact.configs.flat.recommended.languageOptions,
22 | globals: {
23 | ...globals.serviceworker,
24 | ...globals.browser,
25 | },
26 | },
27 | },
28 | {
29 | plugins: {
30 | "react-hooks": pluginReactHooks,
31 | },
32 | settings: { react: { version: "detect" } },
33 | rules: {
34 | ...pluginReactHooks.configs.recommended.rules,
35 | // React scope no longer necessary with new JSX transform.
36 | "react/react-in-jsx-scope": "off",
37 | },
38 | },
39 | ];
40 |
--------------------------------------------------------------------------------
/packages/shared/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies (bun install)
2 | node_modules
3 |
4 | # output
5 | out
6 | dist
7 | *.tgz
8 |
9 | # code coverage
10 | coverage
11 | *.lcov
12 |
13 | # logs
14 | logs
15 | _.log
16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17 |
18 | # dotenv environment variable files
19 | .env
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 | .env.local
24 |
25 | # caches
26 | .eslintcache
27 | .cache
28 | *.tsbuildinfo
29 |
30 | # IntelliJ based IDEs
31 | .idea
32 |
33 | # Finder (MacOS) folder config
34 | .DS_Store
35 |
--------------------------------------------------------------------------------
/packages/shared/README.md:
--------------------------------------------------------------------------------
1 | # shared
2 |
3 | To install dependencies:
4 |
5 | ```bash
6 | bun install
7 | ```
8 |
9 | To run:
10 |
11 | ```bash
12 | bun run index.ts
13 | ```
14 |
15 | This project was created using `bun init` in bun v1.2.15. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
16 |
--------------------------------------------------------------------------------
/packages/shared/index.ts:
--------------------------------------------------------------------------------
1 | export { exampleSchema } from "./schemas/example-schema";
2 | export {
3 | createPlatformSchema,
4 | updatePlatformSchema,
5 | } from "./schemas/platform.schema";
6 |
7 | export {
8 | createDistributorSchema,
9 | updateDistributorSchema,
10 | } from "./schemas/distributor.schema";
11 |
12 | export {
13 | createUserProfileSchema,
14 | updateUserProfileSchema,
15 | } from "./schemas/user-profile.schema";
16 |
17 | export { createGameSchema, updateGameSchema } from "./schemas/game.schema";
18 |
19 | export {
20 | createGameRankSchema,
21 | updateGameRankSchema,
22 | } from "./schemas/gamerank.schema";
23 |
24 | export * from "./schemas/event-attendees";
25 |
26 | export * from "./schemas/event-invitations";
27 | // OpenAPI Response Schemas
28 | export {
29 | ErrorResponseSchema,
30 | GameResponseSchema,
31 | GamesListResponseSchema,
32 | DistributorResponseSchema,
33 | DistributorsListResponseSchema,
34 | PlatformResponseSchema,
35 | PlatformsListResponseSchema,
36 | GameRankResponseSchema,
37 | GameRanksListResponseSchema,
38 | IdParamSchema,
39 | SuccessResponseSchema,
40 | } from "./schemas/openapi-responses.schema";
41 |
--------------------------------------------------------------------------------
/packages/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/shared",
3 | "module": "index.ts",
4 | "type": "module",
5 | "private": true,
6 | "devDependencies": {
7 | "@types/bun": "latest"
8 | },
9 | "peerDependencies": {
10 | "typescript": "^5"
11 | },
12 | "dependencies": {
13 | "zod": "^3.25.48"
14 | },
15 | "exports": {
16 | ".": "./index.ts"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/shared/schemas/distributor.schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const createDistributorSchema = z.object({
4 | name: z
5 | .string()
6 | .min(1, { message: "Name is required" })
7 | .max(255, { message: "Name must be 255 characters or less" })
8 | .trim(),
9 | description: z.string().optional(),
10 | website: z
11 | .string()
12 | .url()
13 | .max(255, { message: "Website must be 255 characters or less" })
14 | .optional(),
15 | logo: z
16 | .string()
17 | .max(255, { message: "Logo must be 255 characters or less" })
18 | .optional(),
19 | });
20 |
21 | export const updateDistributorSchema = z.object({
22 | name: z
23 | .string()
24 | .min(1, { message: "Name cannot be empty" })
25 | .max(255, { message: "Name must be 255 characters or less" })
26 | .trim()
27 | .optional(),
28 | description: z.string().optional().nullable(),
29 | website: z
30 | .string()
31 | .url()
32 | .max(255, { message: "Website must be 255 characters or less" })
33 | .optional()
34 | .nullable(),
35 | logo: z
36 | .string()
37 | .max(255, { message: "Logo must be 255 characters or less" })
38 | .optional()
39 | .nullable(),
40 | });
41 |
--------------------------------------------------------------------------------
/packages/shared/schemas/event-attendees.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const createEventAttendeeSchema = z.object({
4 | eventId: z.coerce.bigint().or(
5 | z
6 | .string()
7 | .regex(/^\d+$/)
8 | .transform((val) => BigInt(val))
9 | ),
10 | userProfileId: z.coerce.bigint().or(
11 | z
12 | .string()
13 | .regex(/^\d+$/)
14 | .transform((val) => BigInt(val))
15 | ),
16 | });
17 |
18 | export const getEventAttendeesSchema = z.object({
19 | page: z.string().optional().default("1"),
20 | limit: z.string().optional().default("10"),
21 | eventId: z.string().optional(),
22 | });
23 |
24 | export type CreateEventAttendeeInput = z.infer<
25 | typeof createEventAttendeeSchema
26 | >;
27 | export type GetEventAttendeesInput = z.infer;
28 |
--------------------------------------------------------------------------------
/packages/shared/schemas/event-invitations.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const createEventInvitationSchema = z.object({
4 | inviterId: z.string().or(z.number()),
5 | inviteeId: z.string().or(z.number()),
6 | eventId: z.string().or(z.number()),
7 | });
8 |
9 | export const respondToInvitationSchema = z.object({
10 | status: z.enum(["accepted", "declined"]),
11 | });
12 |
13 | export const getEventInvitationsSchema = z.object({
14 | page: z.string().optional().default("1"),
15 | limit: z.string().optional().default("10"),
16 | status: z.enum(["pending", "accepted", "declined"]).optional(),
17 | type: z.enum(["sent", "received"]).optional(),
18 | });
19 |
20 | export type CreateEventInvitationInput = z.infer;
21 | export type RespondToInvitationInput = z.infer;
22 | export type GetEventInvitationsInput = z.infer;
--------------------------------------------------------------------------------
/packages/shared/schemas/example-schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const exampleSchema = z.object({
4 | test: z.string(),
5 | });
6 |
--------------------------------------------------------------------------------
/packages/shared/schemas/game.schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const createGameSchema = z.object({
4 | name: z
5 | .string()
6 | .min(1, { message: "Name is required" })
7 | .max(150, { message: "Name must be 150 characters or less" })
8 | .trim(),
9 | description: z.string().optional().nullable(),
10 | logo: z.string().max(255, { message: "Logo must be 255 characters or less" }).optional().nullable(),
11 | });
12 |
13 | export const updateGameSchema = z.object({
14 | name: z
15 | .string()
16 | .min(1, { message: "Name cannot be empty" })
17 | .max(150, { message: "Name must be 150 characters or less" })
18 | .trim()
19 | .optional(),
20 | description: z.string().optional().nullable(),
21 | logo: z.string().max(255, { message: "Logo must be 255 characters or less" }).optional().nullable(),
22 | });
23 |
--------------------------------------------------------------------------------
/packages/shared/schemas/gamerank.schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const createGameRankSchema = z.object({
4 | name: z
5 | .string()
6 | .min(1, { message: "Name is required" })
7 | .max(100, { message: "Name must be 100 characters or less" })
8 | .trim(),
9 | image: z
10 | .string()
11 | .min(1, { message: "Image is required" })
12 | .max(255, { message: "Image must be 255 characters or less" })
13 | .trim(),
14 | order: z
15 | .number()
16 | .int({ message: "Order must be an integer" })
17 | .min(0, { message: "Order must be non-negative" }),
18 | gameId: z
19 | .number()
20 | .int({ message: "Game ID must be an integer" })
21 | .min(1, { message: "Game ID is required" }),
22 | });
23 |
24 | export const updateGameRankSchema = createGameRankSchema.partial();
25 |
--------------------------------------------------------------------------------
/packages/shared/schemas/openapi-responses.schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | // Common error response schema
4 | export const ErrorResponseSchema = z.object({
5 | error: z.string().describe("Error message describing what went wrong"),
6 | });
7 |
8 | // Game response schemas
9 | export const GameResponseSchema = z.object({
10 | id: z.number().int().positive().describe("Unique identifier for the game"),
11 | name: z.string().min(1).max(150).describe("Name of the game"),
12 | description: z.string().nullable().describe("Optional description of the game"),
13 | logo: z.string().max(255).nullable().describe("Optional logo URL for the game"),
14 | createdAt: z.string().datetime().describe("When the game was created"),
15 | updatedAt: z.string().datetime().nullable().describe("When the game was last updated"),
16 | deletedAt: z.string().datetime().nullable().describe("When the game was deleted (soft delete)"),
17 | });
18 |
19 | export const GamesListResponseSchema = z.array(GameResponseSchema);
20 |
21 | // Distributor response schemas
22 | export const DistributorResponseSchema = z.object({
23 | id: z.number().int().positive().describe("Unique identifier for the distributor"),
24 | name: z.string().min(1).max(255).describe("Name of the distributor"),
25 | description: z.string().nullable().describe("Optional description of the distributor"),
26 | website: z.string().max(255).nullable().describe("Optional website URL"),
27 | logo: z.string().max(255).nullable().describe("Optional logo URL"),
28 | createdAt: z.string().datetime().describe("When the distributor was created"),
29 | updatedAt: z.string().datetime().nullable().describe("When the distributor was last updated"),
30 | deletedAt: z.string().datetime().nullable().describe("When the distributor was deleted (soft delete)"),
31 | });
32 |
33 | export const DistributorsListResponseSchema = z.array(DistributorResponseSchema);
34 |
35 | // Platform response schemas
36 | export const PlatformResponseSchema = z.object({
37 | id: z.string().describe("Unique identifier for the platform (BigInt as string)"),
38 | name: z.string().min(1).max(100).describe("Name of the platform"),
39 | createdAt: z.string().datetime().describe("When the platform was created"),
40 | updatedAt: z.string().datetime().nullable().describe("When the platform was last updated"),
41 | deletedAt: z.string().datetime().nullable().describe("When the platform was deleted (soft delete)"),
42 | });
43 |
44 | export const PlatformsListResponseSchema = z.array(PlatformResponseSchema);
45 |
46 | // Game Rank response schemas
47 | export const GameRankResponseSchema = z.object({
48 | id: z.number().int().positive().describe("Unique identifier for the game rank"),
49 | name: z.string().min(1).max(100).describe("Name of the rank"),
50 | image: z.string().min(1).max(255).describe("Image URL for the rank"),
51 | order: z.number().int().min(0).describe("Display order of the rank"),
52 | gameId: z.number().int().positive().describe("ID of the associated game"),
53 | createdAt: z.string().datetime().describe("When the rank was created"),
54 | updatedAt: z.string().datetime().nullable().describe("When the rank was last updated"),
55 | deletedAt: z.string().datetime().nullable().describe("When the rank was deleted (soft delete)"),
56 | });
57 |
58 | export const GameRanksListResponseSchema = z.array(GameRankResponseSchema);
59 |
60 | // Parameter schemas for path parameters
61 | export const IdParamSchema = z.object({
62 | id: z.string().transform((val) => {
63 | const num = parseInt(val, 10);
64 | if (isNaN(num)) throw new Error("Invalid ID format");
65 | return num;
66 | }).describe("Unique identifier"),
67 | });
68 |
69 | // Success message schema
70 | export const SuccessResponseSchema = z.object({
71 | message: z.string().describe("Success message"),
72 | });
73 |
--------------------------------------------------------------------------------
/packages/shared/schemas/platform.schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const createPlatformSchema = z.object({
4 | name: z
5 | .string()
6 | .min(1, { message: "Name is required" })
7 | .max(100, { message: "Name must be 100 characters or less" })
8 | .trim(),
9 | });
10 |
11 | export const updatePlatformSchema = z.object({
12 | name: z
13 | .string()
14 | .min(1, { message: "Name cannot be empty" })
15 | .max(100, { message: "Name must be 100 characters or less" })
16 | .trim()
17 | .optional(),
18 | });
19 |
--------------------------------------------------------------------------------
/packages/shared/schemas/user-profile.schema.ts:
--------------------------------------------------------------------------------
1 | import {z} from 'zod';
2 | import {genderEnum, regionTypeEnum} from '../../../apps/api/src/db/schemas/user-profile.drizzle';
3 |
4 | export const createUserProfileSchema = z.object({
5 | userKeycloakId: z
6 | .string()
7 | .min(1)
8 | .max(100),
9 | birthDate: z
10 | .coerce
11 | .date()
12 | .optional(),
13 | userName: z
14 | .string()
15 | .min(1)
16 | .max(50)
17 | .trim(),
18 | firstName: z
19 | .string()
20 | .max(60)
21 | .trim()
22 | .optional(),
23 | lastName: z
24 | .string()
25 | .max(60)
26 | .trim()
27 | .optional(),
28 | profileImageUrl: z
29 | .string()
30 | .url()
31 | .max(255)
32 | .optional(),
33 | bannerImageUrl: z
34 | .string()
35 | .url()
36 | .max(255)
37 | .optional(),
38 | bio: z
39 | .string()
40 | .max(500)
41 | .optional(),
42 |
43 | gender: z
44 | .enum(genderEnum.enumValues),
45 | regionType: z
46 | .enum(regionTypeEnum.enumValues),
47 |
48 | lastOnline: z.coerce.date().optional(),
49 | });
50 |
51 | export const updateUserProfileSchema = z.object({
52 | userKeycloakId: z
53 | .string()
54 | .min(1)
55 | .max(100)
56 | .optional(),
57 | birthDate: z
58 | .coerce
59 | .date()
60 | .optional(),
61 | userName: z
62 | .string()
63 | .min(1)
64 | .max(50)
65 | .trim()
66 | .optional(),
67 | firstName: z
68 | .string()
69 | .max(60)
70 | .trim()
71 | .optional(),
72 | lastName: z
73 | .string()
74 | .max(60)
75 | .trim()
76 | .optional(),
77 | profileImageUrl: z
78 | .string()
79 | .url()
80 | .max(255)
81 | .optional(),
82 | bannerImageUrl: z
83 | .string()
84 | .url()
85 | .max(255)
86 | .optional(),
87 | bio: z
88 | .string()
89 | .max(500)
90 | .optional(),
91 |
92 | gender: z
93 | .enum(genderEnum.enumValues),
94 | regionType: z
95 | .enum(regionTypeEnum.enumValues),
96 |
97 | lastOnline: z.coerce.date().optional(),
98 | });
--------------------------------------------------------------------------------
/packages/shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Environment setup & latest features
4 | "lib": ["ESNext"],
5 | "target": "ESNext",
6 | "module": "Preserve",
7 | "moduleDetection": "force",
8 | "jsx": "react-jsx",
9 | "allowJs": true,
10 |
11 | // Bundler mode
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "noEmit": true,
16 |
17 | // Best practices
18 | "strict": true,
19 | "skipLibCheck": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedIndexedAccess": true,
22 | "noImplicitOverride": true,
23 |
24 | // Some stricter flags (disabled by default)
25 | "noUnusedLocals": false,
26 | "noUnusedParameters": false,
27 | "noPropertyAccessFromIndexSignature": false
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "declarationMap": true,
6 | "esModuleInterop": true,
7 | "incremental": false,
8 | "isolatedModules": true,
9 | "lib": ["es2022", "DOM", "DOM.Iterable"],
10 | "module": "NodeNext",
11 | "moduleDetection": "force",
12 | "moduleResolution": "NodeNext",
13 | "noUncheckedIndexedAccess": true,
14 | "resolveJsonModule": true,
15 | "skipLibCheck": true,
16 | "strict": true,
17 | "target": "ES2022"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/typescript-config/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "plugins": [{ "name": "next" }],
6 | "module": "ESNext",
7 | "moduleResolution": "Bundler",
8 | "allowJs": true,
9 | "jsx": "preserve",
10 | "noEmit": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/typescript-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/typescript-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/typescript-config/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "jsx": "react-jsx"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/ui/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { config } from "@repo/eslint-config/react-internal";
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | export default config;
5 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/ui",
3 | "version": "0.0.0",
4 | "private": true,
5 | "exports": {
6 | "./*": "./src/*.tsx"
7 | },
8 | "scripts": {
9 | "lint": "eslint . --max-warnings 0",
10 | "generate:component": "turbo gen react-component",
11 | "check-types": "tsc --noEmit"
12 | },
13 | "devDependencies": {
14 | "@repo/eslint-config": "*",
15 | "@repo/typescript-config": "*",
16 | "@turbo/gen": "^2.5.0",
17 | "@types/node": "^22.15.3",
18 | "@types/react": "19.1.6",
19 | "@types/react-dom": "19.1.6",
20 | "eslint": "^9.28.0",
21 | "typescript": "5.8.3"
22 | },
23 | "dependencies": {
24 | "react": "^19.1.0",
25 | "react-dom": "^19.1.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/ui/src/button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ReactNode } from "react";
4 |
5 | interface ButtonProps {
6 | children: ReactNode;
7 | className?: string;
8 | appName: string;
9 | }
10 |
11 | export const Button = ({ children, className, appName }: ButtonProps) => {
12 | return (
13 | alert(`Hello from your ${appName} app!`)}
16 | >
17 | {children}
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/packages/ui/src/card.tsx:
--------------------------------------------------------------------------------
1 | import { type JSX } from "react";
2 |
3 | export function Card({
4 | className,
5 | title,
6 | children,
7 | href,
8 | }: {
9 | className?: string;
10 | title: string;
11 | children: React.ReactNode;
12 | href: string;
13 | }): JSX.Element {
14 | return (
15 |
21 |
22 | {title} ->
23 |
24 | {children}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/packages/ui/src/code.tsx:
--------------------------------------------------------------------------------
1 | import { type JSX } from "react";
2 |
3 | export function Code({
4 | children,
5 | className,
6 | }: {
7 | children: React.ReactNode;
8 | className?: string;
9 | }): JSX.Element {
10 | return {children}
;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/turbo/generators/config.ts:
--------------------------------------------------------------------------------
1 | import type { PlopTypes } from "@turbo/gen";
2 |
3 | // Learn more about Turborepo Generators at https://turborepo.com/docs/guides/generating-code
4 |
5 | export default function generator(plop: PlopTypes.NodePlopAPI): void {
6 | // A simple generator to add a new React component to the internal UI library
7 | plop.setGenerator("react-component", {
8 | description: "Adds a new react component",
9 | prompts: [
10 | {
11 | type: "input",
12 | name: "name",
13 | message: "What is the name of the component?",
14 | },
15 | ],
16 | actions: [
17 | {
18 | type: "add",
19 | path: "src/{{kebabCase name}}.tsx",
20 | templateFile: "templates/component.hbs",
21 | },
22 | {
23 | type: "append",
24 | path: "package.json",
25 | pattern: /"exports": {(?)/g,
26 | template: ' "./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
27 | },
28 | ],
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/packages/ui/turbo/generators/templates/component.hbs:
--------------------------------------------------------------------------------
1 | export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => {
2 | return (
3 |
4 |
{{ pascalCase name }} Component
5 | {children}
6 |
7 | );
8 | };
9 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turborepo.com/schema.json",
3 | "ui": "tui",
4 | "tasks": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "inputs": ["$TURBO_DEFAULT$", ".env*"],
8 | "outputs": [".next/**", "!.next/cache/**"],
9 | "env": ["DATABASE_URL"]
10 | },
11 | "lint": {
12 | "dependsOn": ["^lint"]
13 | },
14 | "check-types": {
15 | "dependsOn": ["^check-types"]
16 | },
17 | "dev": {
18 | "cache": false,
19 | "persistent": true
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------