Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .husky/pre-commit
Comment thread
afif1731 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
npx lint-staged
#!/bin/sh

npx lint-staged || node_modules/.bin/lint-staged
44 changes: 44 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"lint-staged": "lint-staged",
Comment thread
afif1731 marked this conversation as resolved.
Outdated
"prettier:check": "prettier --check .",
"prettier:write": "prettier --write .",
"preview": "vite preview",
Expand Down Expand Up @@ -54,6 +55,7 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
"tone": "^15.1.22",
"yet-another-react-lightbox": "^3.25.0",
"zod": "^4.1.12",
"zustand": "^5.0.8"
Expand Down
7 changes: 7 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ import WhackAMoleGame from "./pages/whack-a-mole";
import CreateWhackAMole from "./pages/whack-a-mole/create";
import EditWhackAMole from "./pages/whack-a-mole/edit";

import GroupSort from "./pages/group-sort/GroupSort";
import CreateGroupSort from "./pages/group-sort/CreateGroupSort";
import EditGroupSort from "./pages/group-sort/EditGroupSort";

import SpeedSorting from "./pages/speed-sorting/SpeedSorting";

import CreateJeopardy from "./pages/jeopardy/CreateJeopardy";
Expand Down Expand Up @@ -100,6 +104,7 @@ function App() {
path="/sliding-puzzle/play/:id"
element={<PlaySlidingPuzzle />}
/>
<Route path="/group-sort/play/:id" element={<GroupSort />} />
<Route path="/jeopardy/play/:id/setup" element={<JeopardyLobby />} />
<Route path="/jeopardy/play/:id" element={<JeopardyBoard />} />
<Route path="/jeopardy/play/:id/end" element={<JeopardyGameEnd />} />
Expand Down Expand Up @@ -193,6 +198,8 @@ function App() {
path="/sliding-puzzle/edit/:id"
element={<EditSlidingPuzzle />}
/>
<Route path="/create-group-sort" element={<CreateGroupSort />} />
<Route path="/group-sort/edit/:id" element={<EditGroupSort />} />
<Route path="/jeopardy/edit/:id" element={<CreateJeopardy />} />
</Route>
</Routes>
Expand Down
17 changes: 8 additions & 9 deletions src/api/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ api.interceptors.request.use(
(config) => {
const token = useAuthStore.getState().token;
const url = config.url || "";
const isPublicRequest = [
"/api/game", // list games (public, optional auth)
"/api/game/template", // templates are public
"/play/public", // public play endpoints
"/leaderboard",
"/check",
].some((p) => url.includes(p));

if (!isPublicRequest && token && config.headers) {

// Only skip token for auth endpoints (register/login)
if (url.includes("/api/auth/register") || url.includes("/api/auth/login")) {
return config;
}

// Send token for ALL other requests (including /api/game for publish/unpublish)
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}

Expand Down
111 changes: 111 additions & 0 deletions src/api/score/index.ts
Comment thread
afif1731 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import api from "../axios";

export interface ISubmitScorePayload {
game_id: string;
score: number;
time_spent?: number;
game_data?: Record<string, unknown>;
}

export interface IGameScore {
id: string;
user_id: string;
game_id: string;
score: number;
time_spent?: number;
game_data?: Record<string, unknown>;
created_at: string;
updated_at: string;
}

export interface ILeaderboardEntry {
user_id: string;
username: string;
highest_score: number;
total_plays: number;
}

export interface IUserScoreSummary {
game_id: string;
game_name: string;
highest_score: number;
total_plays: number;
last_played: string;
}

class ScoreAPI {
static async submitScore(payload: ISubmitScorePayload) {
try {
const response = await api.post(
`/api/game/game-type/group-sort/score/submit`,
payload,
);
return response.data;
} catch (error) {
console.error("Failed to submit score:", error);
throw error;
}
}

static async getHighestScore(gameId: string): Promise<IGameScore | null> {
try {
const response = await api.get(
`/api/game/game-type/group-sort/score/highest/${gameId}`,
);
return response.data.data;
} catch (error) {
console.error("Failed to get highest score:", error);
throw error;
}
}

static async getGameHistory(
gameId: string,
limit: number = 10,
): Promise<IGameScore[]> {
try {
const response = await api.get(
`/api/game/game-type/group-sort/score/history/${gameId}`,
{
params: { limit },
},
);
return response.data.data;
} catch (error) {
console.error("Failed to get game history:", error);
throw error;
}
}

static async getLeaderboard(
gameId: string,
limit: number = 10,
): Promise<ILeaderboardEntry[]> {
try {
const response = await api.get(
`/api/game/game-type/group-sort/score/leaderboard/${gameId}`,
{
params: { limit },
},
);
return response.data.data;
} catch (error) {
console.error("Failed to get leaderboard:", error);
throw error;
}
}

static async getAllUserScores(): Promise<IUserScoreSummary[]> {
try {
const response = await api.get(
`/api/game/game-type/group-sort/score/user/all-scores`,
);
return response.data.data;
} catch (error) {
console.error("Failed to get all user scores:", error);
throw error;
}
}
}

export default ScoreAPI;
13 changes: 9 additions & 4 deletions src/pages/HomePage.tsx
Comment thread
afif1731 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,16 @@ export default function HomePage() {
let imageUrl = thumbnailPlaceholder;

if (game.thumbnail_image && game.thumbnail_image !== "default_image.jpg") {
// Cek apakah URL absolut atau relatif
if (game.thumbnail_image.startsWith("http")) {
// Check if it's base64 data URL
if (game.thumbnail_image.startsWith("data:")) {
imageUrl = game.thumbnail_image;
} else {
// Jika relatif (uploads/...), tambahkan URL Backend
}
// Check if it's already an absolute URL
else if (game.thumbnail_image.startsWith("http")) {
imageUrl = game.thumbnail_image;
}
// Otherwise it's a relative path, add Base URL
else {
imageUrl = `${import.meta.env.VITE_API_URL}/${game.thumbnail_image}`;
}
}
Expand Down
12 changes: 9 additions & 3 deletions src/pages/MyProjectsPage.tsx
Comment thread
afif1731 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,16 @@ export default function MyProjectsPage() {
project.thumbnail_image &&
project.thumbnail_image !== "default_image.jpg"
) {
if (project.thumbnail_image.startsWith("http")) {
// Check if it's base64 data URL
if (project.thumbnail_image.startsWith("data:")) {
imageUrl = project.thumbnail_image;
} else {
// Tambahkan Base URL jika path lokal
}
// Check if it's already an absolute URL
else if (project.thumbnail_image.startsWith("http")) {
imageUrl = project.thumbnail_image;
}
// Otherwise it's a relative path, add Base URL
else {
imageUrl = `${import.meta.env.VITE_API_URL}/${project.thumbnail_image}`;
}
}
Expand Down
Loading
Loading