diff --git a/src/services/mutations/game.ts b/src/services/mutations/game.ts new file mode 100644 index 0000000..785f6f6 --- /dev/null +++ b/src/services/mutations/game.ts @@ -0,0 +1,52 @@ +import { GameRequest } from '@/types/services/game' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-toastify' +import { api } from '../api' + +export const useGamesMutation = () => { + const queryClient = useQueryClient() + + const createGame = useMutation({ + mutationFn: async (newGame: GameRequest) => { + const response = await api.post('/marketing/games', newGame) + return response.data + }, + onSuccess: () => { + toast.success('Game created successfully!') + queryClient.invalidateQueries({ queryKey: ['games'] }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed') + } + }) + + const updateGame = useMutation({ + mutationFn: async ({ id, payload }: { id: string; payload: GameRequest }) => { + const response = await api.put(`/marketing/games/${id}`, payload) + return response.data + }, + onSuccess: () => { + toast.success('Game updated successfully!') + queryClient.invalidateQueries({ queryKey: ['games'] }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed') + } + }) + + const deleteGame = useMutation({ + mutationFn: async (id: string) => { + const response = await api.delete(`/marketing/games/${id}`) + return response.data + }, + onSuccess: () => { + toast.success('Game deleted successfully!') + queryClient.invalidateQueries({ queryKey: ['games'] }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed') + } + }) + + return { createGame, updateGame, deleteGame } +} diff --git a/src/services/queries/game.ts b/src/services/queries/game.ts new file mode 100644 index 0000000..cfd1f5b --- /dev/null +++ b/src/services/queries/game.ts @@ -0,0 +1,46 @@ +import { useQuery } from '@tanstack/react-query' +import { api } from '../api' +import { Game, Games } from '@/types/services/game' + +interface GameQueryParams { + page?: number + limit?: number + search?: string +} + +export function useGames(params: GameQueryParams = {}) { + const { page = 1, limit = 10, search = '', ...filters } = params + + return useQuery({ + queryKey: ['games', { page, limit, search, ...filters }], + queryFn: async () => { + const queryParams = new URLSearchParams() + + queryParams.append('page', page.toString()) + queryParams.append('limit', limit.toString()) + + if (search) { + queryParams.append('search', search) + } + + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + queryParams.append(key, value.toString()) + } + }) + + const res = await api.get(`/marketing/games?${queryParams.toString()}`) + return res.data.data + } + }) +} + +export function useGameById(id: string) { + return useQuery({ + queryKey: ['games', id], + queryFn: async () => { + const res = await api.get(`/marketing/games/${id}`) + return res.data.data + } + }) +} diff --git a/src/types/services/game.ts b/src/types/services/game.ts index c95bf42..bd94dba 100644 --- a/src/types/services/game.ts +++ b/src/types/services/game.ts @@ -15,3 +15,12 @@ export interface Games { limit: number total_pages: number } + +export interface GameRequest { + name: string + type: string + is_active: boolean + metadata: { + imageUrl?: string + } +} diff --git a/src/views/apps/marketing/games/list/AddEditGamesDrawer.tsx b/src/views/apps/marketing/games/list/AddEditGamesDrawer.tsx index 04a2356..6678543 100644 --- a/src/views/apps/marketing/games/list/AddEditGamesDrawer.tsx +++ b/src/views/apps/marketing/games/list/AddEditGamesDrawer.tsx @@ -7,43 +7,21 @@ import Drawer from '@mui/material/Drawer' import IconButton from '@mui/material/IconButton' import MenuItem from '@mui/material/MenuItem' import Typography from '@mui/material/Typography' -import Divider from '@mui/material/Divider' -import Grid from '@mui/material/Grid2' import Box from '@mui/material/Box' import Switch from '@mui/material/Switch' import FormControlLabel from '@mui/material/FormControlLabel' -import Chip from '@mui/material/Chip' -import InputAdornment from '@mui/material/InputAdornment' -import Avatar from '@mui/material/Avatar' -import Card from '@mui/material/Card' -import CardContent from '@mui/material/CardContent' -import FormHelperText from '@mui/material/FormHelperText' // Third-party Imports import { useForm, Controller } from 'react-hook-form' // Component Imports import CustomTextField from '@core/components/mui/TextField' +import ImageUpload from '@/components/ImageUpload' -// Types -export interface Game { - id: string // uuid - name: string - type: string - is_active: boolean - metadata: Record - created_at: string // ISO datetime - updated_at: string // ISO datetime -} - -export interface GameRequest { - name: string - type: string - is_active: boolean - metadata: { - imageUrl?: string - } -} +// Services +import { useFilesMutation } from '@/services/mutations/files' +import { Game, GameRequest } from '@/types/services/game' +import { useGamesMutation } from '@/services/mutations/game' type Props = { open: boolean @@ -67,52 +45,12 @@ const initialData: FormValidateType = { } // Mock mutation hooks (replace with actual hooks) -const useGameMutation = () => { - const createGame = { - mutate: (data: GameRequest, options?: { onSuccess?: () => void }) => { - console.log('Creating game:', data) - setTimeout(() => options?.onSuccess?.(), 1000) - } - } - - const updateGame = { - mutate: (data: { id: string; payload: GameRequest }, options?: { onSuccess?: () => void }) => { - console.log('Updating game:', data) - setTimeout(() => options?.onSuccess?.(), 1000) - } - } - - return { createGame, updateGame } -} // Game types const GAME_TYPES = [ - { value: 'quiz', label: 'Quiz' }, - { value: 'puzzle', label: 'Puzzle' }, - { value: 'memory', label: 'Memory Game' }, - { value: 'trivia', label: 'Trivia' }, - { value: 'word', label: 'Word Game' }, - { value: 'math', label: 'Math Game' }, - { value: 'arcade', label: 'Arcade' }, - { value: 'strategy', label: 'Strategy' } -] - -// Game categories -const GAME_CATEGORIES = [ - { value: 'trivia', label: 'Trivia' }, - { value: 'educational', label: 'Educational' }, - { value: 'entertainment', label: 'Entertainment' }, - { value: 'brain_training', label: 'Brain Training' }, - { value: 'casual', label: 'Casual' }, - { value: 'competitive', label: 'Competitive' } -] - -// Difficulty levels -const DIFFICULTY_LEVELS = [ - { value: 'easy', label: 'Easy' }, - { value: 'medium', label: 'Medium' }, - { value: 'hard', label: 'Hard' }, - { value: 'expert', label: 'Expert' } + { value: 'SPIN', label: 'Spin' }, + { value: 'RUFFLE', label: 'Ruffle' }, + { value: 'MINIGAME', label: 'Mini Game' } ] const AddEditGamesDrawer = (props: Props) => { @@ -120,15 +58,36 @@ const AddEditGamesDrawer = (props: Props) => { const { open, handleClose, data } = props // States - const [showMore, setShowMore] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) - const [imagePreview, setImagePreview] = useState(null) + const [imageUrl, setImageUrl] = useState('') - const { createGame, updateGame } = useGameMutation() + const { createGame, updateGame } = useGamesMutation() + const { mutate, isPending } = useFilesMutation().uploadFile // Determine if this is edit mode const isEditMode = Boolean(data?.id) + // Handle file upload + const handleUpload = async (file: File): Promise => { + return new Promise((resolve, reject) => { + const formData = new FormData() + formData.append('file', file) + formData.append('file_type', 'image') + formData.append('description', 'game image upload') + + mutate(formData, { + onSuccess: r => { + setImageUrl(r.file_url) + setValue('imageUrl', r.file_url) // Update form value + resolve(r.id) + }, + onError: er => { + reject(er) + } + }) + }) + } + // Hooks const { control, @@ -147,42 +106,31 @@ const AddEditGamesDrawer = (props: Props) => { useEffect(() => { if (isEditMode && data) { // Extract imageUrl from metadata - const imageUrl = data.metadata?.imageUrl || '' + const imageUrlFromData = data.metadata?.imageUrl || '' // Populate form with existing data const formData: FormValidateType = { name: data.name || '', - type: data.type || 'quiz', + type: data.type || 'SPIN', is_active: data.is_active ?? true, - imageUrl: imageUrl + imageUrl: imageUrlFromData } resetForm(formData) - setImagePreview(imageUrl || null) + setImageUrl(imageUrlFromData) } else { // Reset to initial data for add mode resetForm(initialData) - setImagePreview(null) + setImageUrl('') } }, [data, isEditMode, resetForm]) - // Handle image URL change + // Sync imageUrl state with form value useEffect(() => { - if (watchedImageUrl) { - setImagePreview(watchedImageUrl) - } else { - setImagePreview(null) + if (watchedImageUrl !== imageUrl) { + setImageUrl(watchedImageUrl) } - }, [watchedImageUrl]) - - // Handle unlimited stock toggle - useEffect(() => { - if (watchedImageUrl) { - setImagePreview(watchedImageUrl) - } else { - setImagePreview(null) - } - }, [watchedImageUrl]) + }, [watchedImageUrl, imageUrl]) const handleFormSubmit = async (formData: FormValidateType) => { try { @@ -229,7 +177,7 @@ const AddEditGamesDrawer = (props: Props) => { const handleReset = () => { handleClose() resetForm(initialData) - setImagePreview(null) + setImageUrl('') } return ( @@ -271,29 +219,6 @@ const AddEditGamesDrawer = (props: Props) => {
- {/* Image Preview */} - {imagePreview && ( - - - - Preview Gambar - - - - - - - )} - {/* Nama Game */}
@@ -336,29 +261,22 @@ const AddEditGamesDrawer = (props: Props) => { />
- {/* Image URL */} + {/* Image Upload */}
- URL Gambar + Gambar Game + ( - - - - ) - }} - /> - )} + render={({ field }) => } />
@@ -392,10 +310,10 @@ const AddEditGamesDrawer = (props: Props) => { }} >
- -
diff --git a/src/views/apps/marketing/games/list/DeleteGamesDialog.tsx b/src/views/apps/marketing/games/list/DeleteGamesDialog.tsx new file mode 100644 index 0000000..9823195 --- /dev/null +++ b/src/views/apps/marketing/games/list/DeleteGamesDialog.tsx @@ -0,0 +1,103 @@ +// React Imports +import { useState } from 'react' + +// MUI Imports +import Dialog from '@mui/material/Dialog' +import DialogTitle from '@mui/material/DialogTitle' +import DialogContent from '@mui/material/DialogContent' +import DialogActions from '@mui/material/DialogActions' +import DialogContentText from '@mui/material/DialogContentText' +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' +import Box from '@mui/material/Box' +import Alert from '@mui/material/Alert' + +// Types +import { Game } from '@/types/services/game' + +type Props = { + open: boolean + onClose: () => void + onConfirm: () => void + game: Game | null + isDeleting?: boolean +} + +const DeleteGameDialog = ({ open, onClose, onConfirm, game, isDeleting = false }: Props) => { + if (!game) return null + + return ( + + + + + Hapus Game + + + + + + Apakah Anda yakin ingin menghapus game berikut? + + + + + {game.name} + + + Dibuat:{' '} + {new Date(game.created_at).toLocaleDateString('id-ID', { + year: 'numeric', + month: 'long', + day: 'numeric' + })} + + + + + + Peringatan: Tindakan ini tidak dapat dibatalkan. Semua data yang terkait dengan game ini + akan dihapus secara permanen. + + + + + Pastikan tidak ada pengguna yang masih menggunakan game ini sebelum menghapus. + + + + + + + + + ) +} + +export default DeleteGameDialog diff --git a/src/views/apps/marketing/games/list/GameListTable.tsx b/src/views/apps/marketing/games/list/GameListTable.tsx index de740e8..d34ed4c 100644 --- a/src/views/apps/marketing/games/list/GameListTable.tsx +++ b/src/views/apps/marketing/games/list/GameListTable.tsx @@ -57,17 +57,10 @@ import { formatCurrency } from '@/utils/transform' import tableStyles from '@core/styles/table.module.css' import Loading from '@/components/layout/shared/Loading' import AddEditGamesDrawer from './AddEditGamesDrawer' - -// Game Interface -export interface Game { - id: string // uuid - name: string - type: string - is_active: boolean - metadata: Record - created_at: string // ISO datetime - updated_at: string // ISO datetime -} +import DeleteGameDialog from './DeleteGamesDialog' +import { Game } from '@/types/services/game' +import { useGames } from '@/services/queries/game' +import { useGamesMutation } from '@/services/mutations/game' declare module '@tanstack/table-core' { interface FilterFns { @@ -127,198 +120,6 @@ const DebouncedInput = ({ return setValue(e.target.value)} /> } -// Dummy data for games -const DUMMY_GAME_DATA: Game[] = [ - { - id: '1', - name: 'Quiz Master Challenge', - type: 'quiz', - is_active: true, - metadata: { - imageUrl: 'https://example.com/quiz-master.jpg' - }, - created_at: '2024-01-15T08:30:00Z', - updated_at: '2024-02-10T10:15:00Z' - }, - { - id: '2', - name: 'Memory Palace', - type: 'memory', - is_active: true, - metadata: { - imageUrl: 'https://example.com/memory-palace.jpg' - }, - created_at: '2024-01-20T09:00:00Z', - updated_at: '2024-02-15T11:30:00Z' - }, - { - id: '3', - name: 'Word Wizard', - type: 'word', - is_active: true, - metadata: {}, - created_at: '2024-01-25T14:20:00Z', - updated_at: '2024-02-20T16:45:00Z' - }, - { - id: '4', - name: 'Math Sprint', - type: 'math', - is_active: true, - metadata: {}, - created_at: '2024-02-01T07:15:00Z', - updated_at: '2024-02-25T13:20:00Z' - }, - { - id: '5', - name: 'Puzzle Paradise', - type: 'puzzle', - is_active: true, - metadata: { - imageUrl: 'https://example.com/puzzle-paradise.jpg' - }, - created_at: '2024-02-05T11:40:00Z', - updated_at: '2024-03-01T09:25:00Z' - }, - { - id: '6', - name: 'Trivia Tournament', - type: 'trivia', - is_active: true, - metadata: {}, - created_at: '2024-02-10T16:00:00Z', - updated_at: '2024-03-05T12:10:00Z' - }, - { - id: '7', - name: 'Speed Arcade', - type: 'arcade', - is_active: true, - metadata: { - imageUrl: 'https://example.com/speed-arcade.jpg' - }, - created_at: '2024-02-15T13:30:00Z', - updated_at: '2024-03-10T15:45:00Z' - }, - { - id: '8', - name: 'Strategy Kingdom', - type: 'strategy', - is_active: true, - metadata: {}, - created_at: '2024-03-01T10:20:00Z', - updated_at: '2024-03-15T08:55:00Z' - }, - { - id: '9', - name: 'Quick Quiz', - type: 'quiz', - is_active: false, - metadata: {}, - created_at: '2024-03-05T12:45:00Z', - updated_at: '2024-03-20T14:30:00Z' - }, - { - id: '10', - name: 'Brain Teaser Deluxe', - type: 'puzzle', - is_active: true, - metadata: { - imageUrl: 'https://example.com/brain-teaser.jpg' - }, - created_at: '2024-03-10T09:15:00Z', - updated_at: '2024-03-25T11:40:00Z' - } -] - -// Mock data hook with dummy data -const useGameCatalog = ({ page, limit, search }: { page: number; limit: number; search: string }) => { - const [isLoading, setIsLoading] = useState(false) - - // Simulate loading - useEffect(() => { - setIsLoading(true) - const timer = setTimeout(() => setIsLoading(false), 500) - return () => clearTimeout(timer) - }, [page, limit, search]) - - // Filter data based on search - const filteredData = useMemo(() => { - if (!search) return DUMMY_GAME_DATA - - return DUMMY_GAME_DATA.filter( - game => - game.name.toLowerCase().includes(search.toLowerCase()) || - game.type.toLowerCase().includes(search.toLowerCase()) || - game.metadata?.description?.toLowerCase().includes(search.toLowerCase()) - ) - }, [search]) - - // Paginate data - const paginatedData = useMemo(() => { - const startIndex = (page - 1) * limit - const endIndex = startIndex + limit - return filteredData.slice(startIndex, endIndex) - }, [filteredData, page, limit]) - - return { - data: { - games: paginatedData, - total_count: filteredData.length - }, - isLoading, - error: null, - isFetching: isLoading - } -} - -// Helper functions -const getGameTypeLabel = (type: string) => { - const typeMap: Record = { - quiz: 'Quiz', - puzzle: 'Puzzle', - memory: 'Memory', - trivia: 'Trivia', - word: 'Word Game', - math: 'Math Game', - arcade: 'Arcade', - strategy: 'Strategy' - } - return typeMap[type] || type -} - -const getDifficultyColor = (difficulty: string): ThemeColor => { - switch (difficulty) { - case 'easy': - return 'success' - case 'medium': - return 'warning' - case 'hard': - return 'error' - case 'expert': - return 'primary' - default: - return 'secondary' - } -} - -const getDifficultyLabel = (difficulty: string) => { - const difficultyMap: Record = { - easy: 'Mudah', - medium: 'Sedang', - hard: 'Sulit', - expert: 'Ahli' - } - return difficultyMap[difficulty] || difficulty -} - -const formatDuration = (seconds: number) => { - if (seconds < 60) return `${seconds}s` - const minutes = Math.floor(seconds / 60) - const remainingSeconds = seconds % 60 - return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m` -} - // Column Definitions const columnHelper = createColumnHelper() @@ -326,19 +127,23 @@ const GameListTable = () => { // States const [addGameOpen, setAddGameOpen] = useState(false) const [editGameData, setEditGameData] = useState(undefined) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [gameToDelete, setGameToDelete] = useState(null) const [rowSelection, setRowSelection] = useState({}) const [globalFilter, setGlobalFilter] = useState('') const [currentPage, setCurrentPage] = useState(1) const [pageSize, setPageSize] = useState(10) const [search, setSearch] = useState('') - const { data, isLoading, error, isFetching } = useGameCatalog({ + const { data, isLoading, error, isFetching } = useGames({ page: currentPage, limit: pageSize, search }) - const games = data?.games ?? [] + const { deleteGame } = useGamesMutation() + + const games = data?.data ?? [] const totalCount = data?.total_count ?? 0 // Hooks @@ -359,14 +164,35 @@ const GameListTable = () => { setAddGameOpen(true) } - const handleDeleteGame = (gameId: string) => { - if (confirm('Apakah Anda yakin ingin menghapus game ini?')) { - console.log('Deleting game:', gameId) - // Add your delete logic here - // deleteGame.mutate(gameId) + const handleDeleteGame = (game: Game) => { + setGameToDelete(game) + setDeleteDialogOpen(true) + } + + const handleConfirmDelete = () => { + if (gameToDelete) { + deleteGame.mutate(gameToDelete.id, { + onSuccess: () => { + console.log('Game deleted successfully') + setDeleteDialogOpen(false) + setGameToDelete(null) + // You might want to refetch data here + // refetch() + }, + onError: error => { + console.error('Error deleting game:', error) + // Handle error (show toast, etc.) + } + }) } } + const handleCloseDeleteDialog = () => { + if (deleteGame.isPending) return // Prevent closing while deleting + setDeleteDialogOpen(false) + setGameToDelete(null) + } + const handleToggleActive = (gameId: string, currentStatus: boolean) => { console.log('Toggling active status for game:', gameId, !currentStatus) // Add your toggle logic here @@ -416,7 +242,7 @@ const GameListTable = () => { - {getGameTypeLabel(row.original.type)} + {row.original.type}
@@ -424,9 +250,7 @@ const GameListTable = () => { }), columnHelper.accessor('type', { header: 'Tipe Game', - cell: ({ row }) => ( - - ) + cell: ({ row }) => }), columnHelper.accessor('is_active', { header: 'Status', @@ -460,14 +284,6 @@ const GameListTable = () => { iconButtonProps={{ size: 'medium' }} iconClassName='text-textSecondary text-[22px]' options={[ - { - text: row.original.is_active ? 'Nonaktifkan' : 'Aktifkan', - icon: row.original.is_active ? 'tabler-eye-off text-[22px]' : 'tabler-eye text-[22px]', - menuItemProps: { - className: 'flex items-center gap-2 text-textSecondary', - onClick: () => handleToggleActive(row.original.id, row.original.is_active) - } - }, { text: 'Edit', icon: 'tabler-edit text-[22px]', @@ -481,7 +297,7 @@ const GameListTable = () => { icon: 'tabler-trash text-[22px]', menuItemProps: { className: 'flex items-center gap-2 text-textSecondary', - onClick: () => handleDeleteGame(row.original.id) + onClick: () => handleDeleteGame(row.original) } } ]} @@ -632,7 +448,18 @@ const GameListTable = () => { disabled={isLoading} /> + + {/* Add/Edit Game Drawer */} + + {/* Delete Game Dialog */} + ) }