Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1,063 changes: 460 additions & 603 deletions package-lock.json

Large diffs are not rendered by default.

41 changes: 37 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import ProfilePage from "./pages/ProfilePage";
import Quiz from "./pages/Quiz";
import Register from "./pages/Register";
import Sandbox from "./pages/Sandbox";

import CreateSpeedSorting from "./pages/speed-sorting/CreateSpeedSorting";
import EditSpeedSorting from "./pages/speed-sorting/EditSpeedSorting";
import SpeedSorting from "./pages/speed-sorting/SpeedSorting";
import ProtectedRoute from "./routes/ProtectedRoutes";

import AirplaneGeneralGame from "./pages/airplane";
Expand Down Expand Up @@ -62,19 +66,18 @@ 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";
import JeopardyLobby from "./pages/jeopardy/JeopardyLobby";
import JeopardyBoard from "./pages/jeopardy/JeopardyBoard";
import JeopardyGameEnd from "./pages/jeopardy/JeopardyGameEnd";
import CreateSpeedSorting from "./pages/speed-sorting/CreateSpeedSorting";
import EditSpeedSorting from "./pages/speed-sorting/EditSpeedSorting";

import CreateCrossword from "./pages/crosswords/create";
import PlayCrossword from "./pages/crosswords/index";
import EditCrossword from "./pages/crosswords/edit";

import CreateWatchMemorize from "./pages/watch-and-memorize/CreateGame";
import EditWatchMemorize from "./pages/watch-and-memorize/EditGame";
import PlayWatchAndMemorize from "./pages/watch-and-memorize/PlayWatchAndMemorize";
// Import halaman Math Generator dari src2
import MathGeneratorPage from "./pages/MathGeneratorPage";
import MathPlay from "./pages/MathPlay";
Expand Down Expand Up @@ -108,6 +111,14 @@ function App() {
<Route path="/flip-tiles/play/:id" element={<FlipTiles />} />
<Route path="/speed-sorting/play/:id" element={<SpeedSorting />} />
<Route path="/anagram/play/:id" element={<PlayAnagram />} />
<Route
path="/pair-or-no-pair/play/:gameId"
element={<PairOrNoPairGame />}
/>
<Route
path="/watch-and-memorize/play/:gameId"
element={<PlayWatchAndMemorize />}
/>
<Route path="/hangman/play/:id" element={<HangmanGame />} />
<Route path="/math-generator/play/:id" element={<MathPlay />} />
<Route
Expand Down Expand Up @@ -135,6 +146,14 @@ function App() {
<Route path="/create-projects" element={<CreateProject />} />

<Route path="/create-quiz" element={<CreateQuiz />} />
<Route
path="/create-speed-sorting"
element={<CreateSpeedSorting />}
/>
<Route
path="/create-pair-or-no-pair"
element={<CreatePairOrNoPair />}
/>
<Route path="/quiz/edit/:id" element={<EditQuiz />} />

<Route
Expand Down Expand Up @@ -205,7 +224,21 @@ function App() {
<Route path="/maze-chase/edit/:id" element={<EditMazeChase />} />

<Route path="/create-anagram" element={<CreateAnagram />} />
<Route
path="/create-watch-and-memorize"
element={<CreateWatchMemorize />}
/>

<Route path="/quiz/edit/:id" element={<EditQuiz />} />
<Route
path="/speed-sorting/edit/:id"
element={<EditSpeedSorting />}
/>
<Route path="/anagram/edit/:id" element={<EditAnagram />} />
<Route
path="/watch-and-memorize/edit/:id"
element={<EditWatchMemorize />}
/>

<Route path="/create-unjumble" element={<CreateUnjumble />} />
<Route path="/unjumble/edit/:id" element={<EditUnjumble />} />
Expand Down
72 changes: 72 additions & 0 deletions src/api/watch-and-memorize/apiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:4000";

export class ApiError extends Error {
status?: number;
data?: unknown;

constructor(message: string, status?: number, data?: unknown) {
super(message);
this.name = "ApiError";
this.status = status;
this.data = data;
}
}

async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const error = await response.json().catch(() => ({
message: response.statusText,
}));
throw new ApiError(
error.message || "Request failed",
response.status,
error,
);
}
return response.json();
}

// ✅ Helper untuk get auth token
function getAuthHeaders(): HeadersInit {
const token = localStorage.getItem("token"); // Sesuaikan dengan storage kamu
return {
"Content-Type": "application/json",
...(token && { Authorization: `Bearer ${token}` }),
};
}

export const apiClient = {
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: "GET",
headers: getAuthHeaders(),
});
return handleResponse<T>(response);
},

async post<T>(endpoint: string, data: unknown): Promise<T> {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: "POST",
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
return handleResponse<T>(response);
},

async put<T>(endpoint: string, data: unknown): Promise<T> {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: "PUT",
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
return handleResponse<T>(response);
},

async delete<T>(endpoint: string): Promise<T> {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: "DELETE",
headers: getAuthHeaders(),
});
return handleResponse<T>(response);
},
};
126 changes: 126 additions & 0 deletions src/api/watch-and-memorize/gameApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { apiClient } from "./apiClient";

// ========== TYPE DEFINITIONS ==========
export interface GameSession {
playerName: string;
score: number;
correctAnswers: number;
totalQuestions: number;
timeSpent: number;
difficulty: string;
coinsEarned: number;
}

export interface LeaderboardEntry {
name: string;
score: number;
difficulty: string;
timeSpent: number;
createdAt?: string;
}

export interface UserProgress {
playerName: string;
coins: number;
highScore: number;
gamesPlayed: number;
pendants: Record<string, number>;
}

export interface CoinsResponse {
userId: string;
username: string;
coins: number;
}

export interface PendantResponse {
id: string;
name: string;
description: string;
price: number;
color: string;
owned?: number;
}

export interface OwnedPendantsResponse {
userId: string;
pendants: PendantResponse[];
}

export interface SubmitGameResponse {
success: boolean;
message: string;
data: {
score: number;
coinsEarned: number;
totalCoins: number;
};
}

export interface LeaderboardResponse {
success: boolean;
data: LeaderboardEntry[];
}

export interface PurchasePendantResponse {
success: boolean;
message: string;
data: {
pendantId: string;
coinsSpent: number;
remainingCoins: number;
};
}

// ========== GAME API ==========
export const gameApi = {
// ✅ COINS - Get user's coins (AUTH REQUIRED)
async getUserCoins(): Promise<CoinsResponse> {
return apiClient.get("/game/game-type/watch-and-memorize/coins");
},

// ✅ SUBMIT RESULT - Submit game & earn coins
async submitGameResult(
gameId: string,
session: GameSession,
): Promise<SubmitGameResponse> {
return apiClient.post(
`/api/game/game-type/watch-and-memorize/${gameId}/submit`,
{
score: session.score,
correctAnswers: session.correctAnswers,
totalQuestions: session.totalQuestions,
timeSpent: session.timeSpent,
coinsEarned: session.coinsEarned,
},
);
},

// ✅ LEADERBOARD - Get game leaderboard (PUBLIC)
async getLeaderboard(
gameId: string,
limit: number = 10,
): Promise<LeaderboardResponse> {
return apiClient.get(
`/api/game/game-type/watch-and-memorize/${gameId}/leaderboard?limit=${limit}`,
);
},

// ✅ PENDANT SHOP - Get available pendants (PUBLIC)
async getAvailablePendants(): Promise<PendantResponse[]> {
return apiClient.get("/game/game-type/watch-and-memorize/pendant/shop");
},

// ✅ OWNED PENDANTS - Get user's pendants (AUTH REQUIRED)
async getOwnedPendants(): Promise<OwnedPendantsResponse> {
return apiClient.get("/game/game-type/watch-and-memorize/pendant/owned");
},

// ✅ PURCHASE PENDANT - Buy pendant with coins (AUTH REQUIRED)
async purchasePendant(pendantId: string): Promise<PurchasePendantResponse> {
return apiClient.post(
"/game/game-type/watch-and-memorize/pendant/purchase",
{ pendantId },
);
},
};
13 changes: 13 additions & 0 deletions src/api/watch-and-memorize/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Export all APIs dan types dari watch-and-memorize module
export { apiClient, ApiError } from "./apiClient";
export { gameApi } from "./gameApi";

// Export types
export type {
GameSession,
LeaderboardEntry,
UserProgress,
CoinsResponse,
PendantResponse,
OwnedPendantsResponse,
} from "./gameApi";
35 changes: 35 additions & 0 deletions src/api/watch-and-memorize/useGetCoins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useState, useEffect } from "react";
import api from "@/api/axios";

interface CoinsResponse {
userId: string;
username: string;
coins: number;
}

export const useGetCoins = () => {
const [data, setData] = useState<CoinsResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
const fetchCoins = async () => {
try {
setIsLoading(true);
const response = await api.get<{ data: CoinsResponse }>(
"/game/game-type/watch-and-memorize/coins",
);
setData(response.data.data);
setError(null);
} catch (err) {
setError(err as Error);
} finally {
setIsLoading(false);
}
};

fetchCoins();
}, []);

return { data, isLoading, error };
};
51 changes: 51 additions & 0 deletions src/api/watch-and-memorize/useGetLeaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useState, useEffect } from "react";
import api from "@/api/axios";

interface LeaderboardEntry {
id: string;
user_id: string;
username: string;
profile_picture?: string;
score: number;
difficulty?: string;
time_taken?: number;
created_at: string;
}

interface LeaderboardResponse {
leaderboard: LeaderboardEntry[];
total: number;
}

export const useGetLeaderboard = (gameId: string, limit: number = 10) => {
const [data, setData] = useState<LeaderboardResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
if (!gameId) {
setIsLoading(false);
return;
}

const fetchLeaderboard = async () => {
try {
setIsLoading(true);
const response = await api.get(
`/api/game/game-type/watch-and-memorize/${gameId}/leaderboard?limit=${limit}`,
);

setData(response.data.data);
setError(null);
} catch (err) {
setError(err as Error);
} finally {
setIsLoading(false);
}
};

fetchLeaderboard();
}, [gameId, limit]);

return { data, isLoading, error };
};
Loading
Loading