Compare commits
61 Commits
nico/6-mar
...
fix-respon
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bc546e985 | |||
| 4fb522f88f | |||
| 85332a8225 | |||
| 363bfa65fb | |||
| f076b81d14 | |||
| 64b116588b | |||
| 4a7811e06f | |||
| 3803c79c95 | |||
| 0160fa636d | |||
| 3684e83187 | |||
| 77c54b5c8a | |||
| bb80b0ecc1 | |||
| 1b59d6bf09 | |||
| eb1ad54db6 | |||
| 21ec3ad1c1 | |||
| 3a115908c4 | |||
| 5ff791642c | |||
| b803c7a90c | |||
| fb2fe67c23 | |||
| 51460558d4 | |||
| d105ceeb6b | |||
| c865aee766 | |||
| 273dfdfd09 | |||
| 1d1d8e50dc | |||
| 092afe67d2 | |||
| 2d9170705d | |||
| fdf9a951a4 | |||
| ca74029688 | |||
| 1a8fc1a670 | |||
| 19235f0791 | |||
| 61de7d8d33 | |||
| 8fb85ce56c | |||
| 1f98b6993d | |||
| f3a10d63d1 | |||
| 7a42bec63b | |||
| 44c421129e | |||
| ddff427926 | |||
| 00c8caade4 | |||
| 0209f49449 | |||
| 344c6ada6d | |||
| 11acd04419 | |||
| 8d49213b68 | |||
| 96911e3cf1 | |||
| 9950c28b9b | |||
| fa0f3538d1 | |||
| 2778f53aff | |||
| 37ac91d4f4 | |||
| 217f4a9a3b | |||
| 5d6a7437ed | |||
| 752a6cabee | |||
| 134ddc6154 | |||
| 28979c6b49 | |||
| b2066caa13 | |||
| 023c77d636 | |||
| 9bf3ec72cf | |||
| f359f5b1ce | |||
| 1c1e8fb190 | |||
| 54f83da3b8 | |||
| f8985c550f | |||
| e3d909e760 | |||
| 16a8df50c1 |
25
src/app/admin/(dashboard)/user&role/_com/getMenuIdByRole.ts
Normal file
25
src/app/admin/(dashboard)/user&role/_com/getMenuIdByRole.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// src/app/admin/_com/getMenuIdsByRoleId.ts
|
||||||
|
import { navBar, role1, role2, role3 } from '@/app/admin/_com/list_PageAdmin';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengembalikan daftar ID menu (string[]) berdasarkan roleId
|
||||||
|
*/
|
||||||
|
export function getMenuIdsByRoleId(roleId: string | number): string[] {
|
||||||
|
const id = typeof roleId === 'string' ? parseInt(roleId, 10) : roleId;
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case 0:
|
||||||
|
// Asumsikan devBar ada dan punya struktur sama
|
||||||
|
return []; // atau sesuaikan jika ada devBar
|
||||||
|
case 1:
|
||||||
|
return navBar.map(section => section.id);
|
||||||
|
case 2:
|
||||||
|
return role1.map(section => section.id);
|
||||||
|
case 3:
|
||||||
|
return role2.map(section => section.id);
|
||||||
|
case 4:
|
||||||
|
return role3.map(section => section.id);
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { getMenuIdsByRoleId } from "@/app/admin/(dashboard)/user&role/_com/getMenuIdByRole";
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
@@ -34,11 +35,25 @@ export default async function userUpdate(context: Context) {
|
|||||||
const isActiveChanged =
|
const isActiveChanged =
|
||||||
isActive !== undefined && currentUser.isActive !== isActive;
|
isActive !== undefined && currentUser.isActive !== isActive;
|
||||||
|
|
||||||
// ✅ Jika role berubah, hapus semua akses menu yang ada
|
// ✅ Jika role berubah, reset dan set ulang akses menu
|
||||||
if (isRoleChanged) {
|
if (isRoleChanged && roleId) {
|
||||||
|
// Hapus akses lama
|
||||||
await prisma.userMenuAccess.deleteMany({
|
await prisma.userMenuAccess.deleteMany({
|
||||||
where: { userId: id }
|
where: { userId: id }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ambil menu default untuk role baru
|
||||||
|
const menuIds = getMenuIdsByRoleId(roleId);
|
||||||
|
|
||||||
|
if (menuIds.length > 0) {
|
||||||
|
// Buat akses baru
|
||||||
|
await prisma.userMenuAccess.createMany({
|
||||||
|
data: menuIds.map(menuId => ({
|
||||||
|
userId: id,
|
||||||
|
menuId
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user
|
// Update user
|
||||||
|
|||||||
@@ -92,10 +92,10 @@ const MusicPlayer = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
<Box px={{ base: 'xs', sm: 'md', md: 100 }} py="xl">
|
||||||
<Paper
|
<Paper
|
||||||
mx="auto"
|
mx="auto"
|
||||||
p="xl"
|
p={{ base: 'md', sm: 'xl' }}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
bg="white"
|
bg="white"
|
||||||
@@ -105,42 +105,52 @@ const MusicPlayer = () => {
|
|||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<BackButton />
|
<BackButton />
|
||||||
<Group justify="space-between" mb="xl" mt={"md"}>
|
<Flex
|
||||||
|
justify="space-between"
|
||||||
|
align={{ base: 'flex-start', sm: 'center' }}
|
||||||
|
direction={{ base: 'column', sm: 'row' }}
|
||||||
|
gap="md"
|
||||||
|
mb="xl"
|
||||||
|
mt="md"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Text size="32px" fw={700} c="#0B4F78">Selamat Datang Kembali</Text>
|
<Text fz={{ base: '24px', sm: '32px' }} fw={700} c="#0B4F78" lh={1.2}>Selamat Datang Kembali</Text>
|
||||||
<Text size="md" c="#5A6C7D">Temukan musik favorit Anda hari ini</Text>
|
<Text size="sm" c="#5A6C7D">Temukan musik favorit Anda hari ini</Text>
|
||||||
</div>
|
</div>
|
||||||
<Group gap="md">
|
<TextInput
|
||||||
<TextInput
|
placeholder="Cari lagu..."
|
||||||
placeholder="Cari lagu..."
|
leftSection={<IconSearch size={18} />}
|
||||||
leftSection={<IconSearch size={18} />}
|
radius="xl"
|
||||||
radius="xl"
|
w={{ base: '100%', sm: 280 }}
|
||||||
w={280}
|
value={search}
|
||||||
value={search}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
styles={{ input: { backgroundColor: '#fff' } }}
|
||||||
styles={{ input: { backgroundColor: '#fff' } }}
|
/>
|
||||||
/>
|
</Flex>
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<div>
|
<div>
|
||||||
<Text size="xl" fw={700} c="#0B4F78" mb="md">Sedang Diputar</Text>
|
<Text size="xl" fw={700} c="#0B4F78" mb="md">Sedang Diputar</Text>
|
||||||
{currentSong ? (
|
{currentSong ? (
|
||||||
<Card radius="md" p="xl" shadow="md">
|
<Card radius="md" p={{ base: 'md', sm: 'xl' }} shadow="md" withBorder>
|
||||||
<Group align="center" gap="xl">
|
<Flex
|
||||||
|
direction={{ base: 'column', sm: 'row' }}
|
||||||
|
align="center"
|
||||||
|
gap={{ base: 'md', sm: 'xl' }}
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={currentSong.coverImage?.link || '/mp3-logo.png'}
|
src={currentSong.coverImage?.link || '/mp3-logo.png'}
|
||||||
size={180}
|
size={120}
|
||||||
radius="md"
|
radius="md"
|
||||||
/>
|
/>
|
||||||
<Stack gap="md" style={{ flex: 1 }}>
|
<Stack gap="md" style={{ flex: 1, width: '100%' }}>
|
||||||
<div>
|
<Box ta={{ base: 'center', sm: 'left' }}>
|
||||||
<Text size="28px" fw={700} c="#0B4F78">{currentSong.judul}</Text>
|
<Text fz={{ base: '20px', sm: '28px' }} fw={700} c="#0B4F78" lineClamp={1}>{currentSong.judul}</Text>
|
||||||
<Text size="lg" c="#5A6C7D">{currentSong.artis}</Text>
|
<Text size="lg" c="#5A6C7D">{currentSong.artis}</Text>
|
||||||
{currentSong.genre && (
|
{currentSong.genre && (
|
||||||
<Badge mt="xs" color="#0B4F78" variant="light">{currentSong.genre}</Badge>
|
<Badge mt="xs" color="#0B4F78" variant="light">{currentSong.genre}</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Text size="xs" c="#5A6C7D" w={42}>{formatTime(currentTime)}</Text>
|
<Text size="xs" c="#5A6C7D" w={42}>{formatTime(currentTime)}</Text>
|
||||||
<Slider
|
<Slider
|
||||||
@@ -155,7 +165,7 @@ const MusicPlayer = () => {
|
|||||||
<Text size="xs" c="#5A6C7D" w={42}>{formatTime(duration || 0)}</Text>
|
<Text size="xs" c="#5A6C7D" w={42}>{formatTime(duration || 0)}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card radius="md" p="xl" shadow="md">
|
<Card radius="md" p="xl" shadow="md">
|
||||||
@@ -175,28 +185,29 @@ const MusicPlayer = () => {
|
|||||||
<Grid.Col span={{ base: 12, sm: 6, lg: 4 }} key={song.id}>
|
<Grid.Col span={{ base: 12, sm: 6, lg: 4 }} key={song.id}>
|
||||||
<Card
|
<Card
|
||||||
radius="md"
|
radius="md"
|
||||||
p="md"
|
p="sm"
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
|
withBorder
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
border: currentSong?.id === song.id ? '2px solid #0B4F78' : '2px solid transparent',
|
borderColor: currentSong?.id === song.id ? '#0B4F78' : 'transparent',
|
||||||
|
backgroundColor: currentSong?.id === song.id ? '#F0F7FA' : 'white',
|
||||||
transition: 'all 0.2s'
|
transition: 'all 0.2s'
|
||||||
}}
|
}}
|
||||||
onClick={() => playSong(song)}
|
onClick={() => playSong(song)}
|
||||||
>
|
>
|
||||||
<Group gap="md" align="center">
|
<Group gap="sm" align="center" wrap="nowrap">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={song.coverImage?.link || 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop'}
|
src={song.coverImage?.link || '/mp3-logo.png'}
|
||||||
size={64}
|
size={50}
|
||||||
radius="md"
|
radius="md"
|
||||||
/>
|
/>
|
||||||
<Stack gap={4} style={{ flex: 1, minWidth: 0 }}>
|
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text size="sm" fw={600} c="#0B4F78" truncate>{song.judul}</Text>
|
<Text size="sm" fw={600} c="#0B4F78" truncate>{song.judul}</Text>
|
||||||
<Text size="xs" c="#5A6C7D">{song.artis}</Text>
|
<Text size="xs" c="#5A6C7D" truncate>{song.artis}</Text>
|
||||||
<Text size="xs" c="#8A9BA8">{song.durasi}</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
{currentSong?.id === song.id && isPlaying && (
|
{currentSong?.id === song.id && isPlaying && (
|
||||||
<Badge color="#0B4F78" variant="filled">Memutar</Badge>
|
<Badge color="#0B4F78" variant="filled" size="xs">Playing</Badge>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -207,34 +218,42 @@ const MusicPlayer = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
{/* Control Player Section */}
|
||||||
<Paper
|
<Paper
|
||||||
mt="xl"
|
mt="xl"
|
||||||
mx="auto"
|
mx="auto"
|
||||||
p="xl"
|
p={{ base: 'md', sm: 'xl' }}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
bg="white"
|
bg="white"
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid #eaeaea',
|
border: '1px solid #eaeaea',
|
||||||
|
position: 'sticky',
|
||||||
|
bottom: 20,
|
||||||
|
zIndex: 10
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex align="center" justify="space-between" gap="xl" h="100%">
|
<Flex
|
||||||
<Group gap="md" style={{ flex: 1 }}>
|
direction={{ base: 'column', md: 'row' }}
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
gap={{ base: 'md', md: 'xl' }}
|
||||||
|
>
|
||||||
|
{/* Song Info */}
|
||||||
|
<Group gap="md" style={{ flex: 1, width: '100%' }} wrap="nowrap">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={currentSong?.coverImage?.link || 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop'}
|
src={currentSong?.coverImage?.link || '/mp3-logo.png'}
|
||||||
size={56}
|
size={48}
|
||||||
radius="md"
|
radius="md"
|
||||||
/>
|
/>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
{currentSong ? (
|
{currentSong ? (
|
||||||
<>
|
<>
|
||||||
<Text size="sm" fw={600} c="#0B4F78" truncate>{currentSong.judul}</Text>
|
<Text size="sm" fw={600} c="#0B4F78" truncate>{currentSong.judul}</Text>
|
||||||
<Text size="xs" c="#5A6C7D">{currentSong.artis}</Text>
|
<Text size="xs" c="#5A6C7D" truncate>{currentSong.artis}</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Text size="sm" c="dimmed">Tidak ada lagu</Text>
|
<Text size="sm" c="dimmed">Tidak ada lagu</Text>
|
||||||
@@ -242,29 +261,31 @@ const MusicPlayer = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Stack gap="xs" style={{ flex: 1 }} align="center">
|
{/* Controls + Progress */}
|
||||||
<Group gap="md">
|
<Stack gap="xs" style={{ flex: 2, width: '100%' }} align="center">
|
||||||
|
<Group gap="sm">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={isShuffle ? 'filled' : 'subtle'}
|
variant={isShuffle ? 'filled' : 'subtle'}
|
||||||
color="#0B4F78"
|
color="#0B4F78"
|
||||||
onClick={toggleShuffleHandler}
|
onClick={toggleShuffleHandler}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
|
size={48}
|
||||||
>
|
>
|
||||||
{isShuffle ? <IconArrowsShuffle size={18} /> : <IconX size={18} />}
|
{isShuffle ? <IconArrowsShuffle size={18} /> : <IconX size={18} />}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<ActionIcon variant="light" color="#0B4F78" size={40} radius="xl" onClick={skipBack}>
|
<ActionIcon variant="light" color="#0B4F78" size={48} radius="xl" onClick={skipBack}>
|
||||||
<IconPlayerSkipBackFilled size={20} />
|
<IconPlayerSkipBackFilled size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="#0B4F78"
|
color="#0B4F78"
|
||||||
size={56}
|
size={48}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={togglePlayPauseHandler}
|
onClick={togglePlayPauseHandler}
|
||||||
>
|
>
|
||||||
{isPlaying ? <IconPlayerPauseFilled size={26} /> : <IconPlayerPlayFilled size={26} />}
|
{isPlaying ? <IconPlayerPauseFilled size={26} /> : <IconPlayerPlayFilled size={26} />}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<ActionIcon variant="light" color="#0B4F78" size={40} radius="xl" onClick={skipForward}>
|
<ActionIcon variant="light" color="#0B4F78" size={48} radius="xl" onClick={skipForward}>
|
||||||
<IconPlayerSkipForwardFilled size={20} />
|
<IconPlayerSkipForwardFilled size={20} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
@@ -272,6 +293,7 @@ const MusicPlayer = () => {
|
|||||||
color="#0B4F78"
|
color="#0B4F78"
|
||||||
onClick={toggleRepeatHandler}
|
onClick={toggleRepeatHandler}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
{isRepeat ? <IconRepeat size={18} /> : <IconRepeatOff size={18} />}
|
{isRepeat ? <IconRepeat size={18} /> : <IconRepeatOff size={18} />}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -290,7 +312,8 @@ const MusicPlayer = () => {
|
|||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Group gap="xs" style={{ flex: 1 }} justify="flex-end">
|
{/* Volume Control - Hidden on mobile, shown on md and up */}
|
||||||
|
<Group gap="xs" style={{ flex: 1 }} justify="flex-end" visibleFrom="md">
|
||||||
<ActionIcon variant="subtle" color="gray" onClick={toggleMuteHandler}>
|
<ActionIcon variant="subtle" color="gray" onClick={toggleMuteHandler}>
|
||||||
{isMuted || volume === 0 ? <IconVolumeOff size={20} /> : <IconVolume size={20} />}
|
{isMuted || volume === 0 ? <IconVolumeOff size={20} /> : <IconVolume size={20} />}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -93,28 +93,19 @@ export default function FixedPlayerBar() {
|
|||||||
mt="md"
|
mt="md"
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: '50%', // Menempatkan titik atas ikon di tengah layar
|
top: '50%',
|
||||||
left: '0px',
|
left: '0px',
|
||||||
transform: 'translateY(-50%)', // Menggeser ikon ke atas sebesar setengah tingginya sendiri agar benar-benar di tengah
|
transform: 'translateY(-50%)',
|
||||||
borderBottomRightRadius: '20px',
|
borderBottomRightRadius: '20px',
|
||||||
borderTopRightRadius: '20px',
|
borderTopRightRadius: '20px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'transform 0.2s ease',
|
transition: 'transform 0.2s ease',
|
||||||
zIndex: 1
|
zIndex: 1000 // Higher z-index
|
||||||
}}
|
}}
|
||||||
onClick={handleRestorePlayer}
|
onClick={handleRestorePlayer}
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.transform = 'translateY(-50%) scale(1.1)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.transform = 'translateY(-50%)';
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<IconMusic size={28} color="white" />
|
<IconMusic size={24} color="white" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Spacer to prevent content from being hidden behind player */}
|
|
||||||
<Box h={20} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -131,132 +122,125 @@ export default function FixedPlayerBar() {
|
|||||||
bottom={0}
|
bottom={0}
|
||||||
left={0}
|
left={0}
|
||||||
right={0}
|
right={0}
|
||||||
p="sm"
|
p={{ base: 'xs', sm: 'sm' }}
|
||||||
shadow="lg"
|
shadow="xl"
|
||||||
style={{
|
style={{
|
||||||
zIndex: 1,
|
zIndex: 1000,
|
||||||
borderTop: '1px solid rgba(0,0,0,0.1)',
|
borderTop: '1px solid rgba(0,0,0,0.1)',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex align="center" gap="md" justify="space-between">
|
<Flex align="center" gap={{ base: 'xs', sm: 'md' }} justify="space-between">
|
||||||
{/* Song Info - Left */}
|
{/* Song Info - Left */}
|
||||||
<Group gap="sm" flex={1} style={{ minWidth: 0 }}>
|
<Group gap="xs" flex={{ base: 2, sm: 1 }} style={{ minWidth: 0 }} wrap="nowrap">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={currentSong.coverImage?.link || ''}
|
src={currentSong.coverImage?.link || ''}
|
||||||
alt={currentSong.judul}
|
alt={currentSong.judul}
|
||||||
size={40}
|
size={"36"}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
imageProps={{ loading: 'lazy' }}
|
|
||||||
/>
|
/>
|
||||||
<Box style={{ minWidth: 0 }}>
|
<Box style={{ minWidth: 0, flex: 1 }}>
|
||||||
<Text fz="sm" fw={600} truncate>
|
<Text fz={{ base: 'xs', sm: 'sm' }} fw={600} truncate>
|
||||||
{currentSong.judul}
|
{currentSong.judul}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="xs" c="dimmed" truncate>
|
<Text fz="10px" c="dimmed" truncate>
|
||||||
{currentSong.artis}
|
{currentSong.artis}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Controls + Progress - Center */}
|
{/* Controls - Center */}
|
||||||
<Group gap="xs" flex={2} justify="center">
|
<Group gap={"xs"} flex={{ base: 1, sm: 2 }} justify="center" wrap="nowrap">
|
||||||
{/* Control Buttons */}
|
{/* Shuffle - Desktop Only */}
|
||||||
<Group gap="xs">
|
<ActionIcon
|
||||||
<ActionIcon
|
variant={isShuffle ? 'filled' : 'subtle'}
|
||||||
variant={isShuffle ? 'filled' : 'subtle'}
|
color={isShuffle ? '#0B4F78' : 'gray'}
|
||||||
color={isShuffle ? 'blue' : 'gray'}
|
size={"md"}
|
||||||
size="lg"
|
onClick={handleToggleShuffle}
|
||||||
onClick={handleToggleShuffle}
|
visibleFrom="sm"
|
||||||
title="Shuffle"
|
>
|
||||||
>
|
<IconArrowsShuffle size={18} />
|
||||||
<IconArrowsShuffle size={18} />
|
</ActionIcon>
|
||||||
</ActionIcon>
|
|
||||||
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color="gray"
|
color="gray"
|
||||||
size="lg"
|
size={"md"}
|
||||||
onClick={playPrev}
|
onClick={playPrev}
|
||||||
title="Previous"
|
>
|
||||||
>
|
<IconPlayerSkipBackFilled size={20} />
|
||||||
<IconPlayerSkipBackFilled size={20} />
|
</ActionIcon>
|
||||||
</ActionIcon>
|
|
||||||
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color={isPlaying ? 'blue' : 'gray'}
|
color="#0B4F78"
|
||||||
size="xl"
|
size={"lg"}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={togglePlayPause}
|
onClick={togglePlayPause}
|
||||||
title={isPlaying ? 'Pause' : 'Play'}
|
>
|
||||||
>
|
{isPlaying ? (
|
||||||
{isPlaying ? (
|
<IconPlayerPauseFilled size={24} />
|
||||||
<IconPlayerPauseFilled size={24} />
|
) : (
|
||||||
) : (
|
<IconPlayerPlayFilled size={24} />
|
||||||
<IconPlayerPlayFilled size={24} />
|
)}
|
||||||
)}
|
</ActionIcon>
|
||||||
</ActionIcon>
|
|
||||||
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color="gray"
|
color="gray"
|
||||||
size="lg"
|
size={"md"}
|
||||||
onClick={playNext}
|
onClick={playNext}
|
||||||
title="Next"
|
>
|
||||||
>
|
<IconPlayerSkipForwardFilled size={20} />
|
||||||
<IconPlayerSkipForwardFilled size={20} />
|
</ActionIcon>
|
||||||
</ActionIcon>
|
|
||||||
|
|
||||||
<ActionIcon
|
{/* Repeat - Desktop Only */}
|
||||||
variant="subtle"
|
<ActionIcon
|
||||||
color={isRepeat ? 'blue' : 'gray'}
|
variant={isRepeat ? 'filled' : 'subtle'}
|
||||||
size="lg"
|
color={isRepeat ? '#0B4F78' : 'gray'}
|
||||||
onClick={toggleRepeat}
|
size={"md"}
|
||||||
title={isRepeat ? 'Repeat On' : 'Repeat Off'}
|
onClick={toggleRepeat}
|
||||||
>
|
visibleFrom="sm"
|
||||||
{isRepeat ? <IconRepeat size={18} /> : <IconRepeatOff size={18} />}
|
>
|
||||||
</ActionIcon>
|
{isRepeat ? <IconRepeat size={18} /> : <IconRepeatOff size={18} />}
|
||||||
</Group>
|
</ActionIcon>
|
||||||
|
|
||||||
{/* Progress Bar - Desktop */}
|
{/* Progress Bar - Desktop Only */}
|
||||||
<Box w={200} display={{ base: 'none', md: 'block' }}>
|
<Box w={150} ml="md" visibleFrom="md">
|
||||||
<Slider
|
<Slider
|
||||||
value={currentTime}
|
value={currentTime}
|
||||||
max={duration || 100}
|
max={duration || 100}
|
||||||
onChange={handleSeek}
|
onChange={handleSeek}
|
||||||
size="sm"
|
size="xs"
|
||||||
color="blue"
|
color="#0B4F78"
|
||||||
label={(value) => formatTime(value)}
|
label={(value) => formatTime(value)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Right Controls - Volume + Close */}
|
{/* Right Controls - Volume + Close */}
|
||||||
<Group gap="xs" flex={1} justify="flex-end">
|
<Group gap={4} flex={1} justify="flex-end" wrap="nowrap">
|
||||||
|
{/* Volume Control - Tablet/Desktop */}
|
||||||
<Box
|
<Box
|
||||||
onMouseEnter={() => setShowVolume(true)}
|
onMouseEnter={() => setShowVolume(true)}
|
||||||
onMouseLeave={() => setShowVolume(false)}
|
onMouseLeave={() => setShowVolume(false)}
|
||||||
pos="relative"
|
pos="relative"
|
||||||
|
visibleFrom="sm"
|
||||||
>
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color={isMuted ? 'red' : 'gray'}
|
color={isMuted ? 'red' : 'gray'}
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={toggleMute}
|
onClick={toggleMute}
|
||||||
title={isMuted ? 'Unmute' : 'Mute'}
|
|
||||||
>
|
>
|
||||||
{isMuted ? (
|
{isMuted ? <IconVolumeOff size={18} /> : <IconVolume size={18} />}
|
||||||
<IconVolumeOff size={18} />
|
|
||||||
) : (
|
|
||||||
<IconVolume size={18} />
|
|
||||||
)}
|
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
mounted={showVolume}
|
mounted={showVolume}
|
||||||
transition="scale-y"
|
transition="scale-y"
|
||||||
duration={200}
|
duration={200}
|
||||||
timingFunction="ease"
|
|
||||||
>
|
>
|
||||||
{(style) => (
|
{(style) => (
|
||||||
<Paper
|
<Paper
|
||||||
@@ -265,8 +249,8 @@ export default function FixedPlayerBar() {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '100%',
|
bottom: '100%',
|
||||||
right: 0,
|
right: 0,
|
||||||
mb: 'xs',
|
marginBottom: '10px',
|
||||||
p: 'sm',
|
padding: '10px',
|
||||||
zIndex: 1001,
|
zIndex: 1001,
|
||||||
}}
|
}}
|
||||||
shadow="md"
|
shadow="md"
|
||||||
@@ -276,8 +260,8 @@ export default function FixedPlayerBar() {
|
|||||||
value={isMuted ? 0 : volume}
|
value={isMuted ? 0 : volume}
|
||||||
max={100}
|
max={100}
|
||||||
onChange={handleVolumeChange}
|
onChange={handleVolumeChange}
|
||||||
h={100}
|
h={80}
|
||||||
color="blue"
|
color="#0B4F78"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -288,30 +272,29 @@ export default function FixedPlayerBar() {
|
|||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color="gray"
|
color="gray"
|
||||||
size="lg"
|
size={"md"}
|
||||||
onClick={handleMinimizePlayer}
|
onClick={handleMinimizePlayer}
|
||||||
title="Minimize player"
|
|
||||||
>
|
>
|
||||||
<IconX size={18} />
|
<IconX size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* Progress Bar - Mobile */}
|
{/* Progress Bar - Mobile (Base) */}
|
||||||
<Box mt="xs" display={{ base: 'block', md: 'none' }}>
|
<Box px="xs" mt={4} hiddenFrom="md">
|
||||||
<Slider
|
<Slider
|
||||||
value={currentTime}
|
value={currentTime}
|
||||||
max={duration || 100}
|
max={duration || 100}
|
||||||
onChange={handleSeek}
|
onChange={handleSeek}
|
||||||
size="sm"
|
size="xs"
|
||||||
color="blue"
|
color="#0B4F78"
|
||||||
label={(value) => formatTime(value)}
|
label={(value) => formatTime(value)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Spacer to prevent content from being hidden behind player */}
|
{/* Spacer to prevent content from being hidden behind player */}
|
||||||
<Box h={80} />
|
<Box h={{ base: 70, sm: 80 }} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Paper, Table, Title } from '@mantine/core';
|
import { Paper, Table, Title, Text } from '@mantine/core';
|
||||||
|
|
||||||
function Section({ title, data }: any) {
|
function Section({ title, data }: any) {
|
||||||
if (!data || data.length === 0) return null;
|
if (!data || data.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Table.Tr>
|
<Table.Tr bg="gray.0">
|
||||||
<Table.Td colSpan={2}>
|
<Table.Td colSpan={2}>
|
||||||
<strong>{title}</strong>
|
<Text fw={700} fz={{ base: 'xs', sm: 'sm' }}>{title}</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
|
|
||||||
{data.map((item: any) => (
|
{data.map((item: any) => (
|
||||||
<Table.Tr key={item.id}>
|
<Table.Tr key={item.id}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{item.kode} - {item.uraian}
|
<Text fz={{ base: 'xs', sm: 'sm' }} lineClamp={2}>
|
||||||
|
{item.kode} - {item.uraian}
|
||||||
|
</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td ta="right">
|
<Table.Td ta="right">
|
||||||
Rp {item.anggaran.toLocaleString('id-ID')}
|
<Text fz={{ base: 'xs', sm: 'sm' }} fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||||
|
Rp {item.anggaran.toLocaleString('id-ID')}
|
||||||
|
</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
@@ -39,22 +43,24 @@ export default function PaguTable({ apbdesData }: any) {
|
|||||||
const pembiayaan = items.filter((i: any) => i.tipe === 'pembiayaan');
|
const pembiayaan = items.filter((i: any) => i.tipe === 'pembiayaan');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper withBorder p="md" radius="md">
|
<Paper withBorder p={{ base: 'sm', sm: 'md' }} radius="md">
|
||||||
<Title order={5} mb="md">{title}</Title>
|
<Title order={5} mb="md" fz={{ base: 'sm', sm: 'md' }}>{title}</Title>
|
||||||
|
|
||||||
<Table>
|
<Table.ScrollContainer minWidth={280}>
|
||||||
<Table.Thead>
|
<Table verticalSpacing="xs">
|
||||||
<Table.Tr>
|
<Table.Thead>
|
||||||
<Table.Th>Uraian</Table.Th>
|
<Table.Tr>
|
||||||
<Table.Th ta="right">Anggaran (Rp)</Table.Th>
|
<Table.Th fz={{ base: 'xs', sm: 'sm' }}>Uraian</Table.Th>
|
||||||
</Table.Tr>
|
<Table.Th ta="right" fz={{ base: 'xs', sm: 'sm' }}>Anggaran (Rp)</Table.Th>
|
||||||
</Table.Thead>
|
</Table.Tr>
|
||||||
<Table.Tbody>
|
</Table.Thead>
|
||||||
<Section title="1) PENDAPATAN" data={pendapatan} />
|
<Table.Tbody>
|
||||||
<Section title="2) BELANJA" data={belanja} />
|
<Section title="1) PENDAPATAN" data={pendapatan} />
|
||||||
<Section title="3) PEMBIAYAAN" data={pembiayaan} />
|
<Section title="2) BELANJA" data={belanja} />
|
||||||
</Table.Tbody>
|
<Section title="3) PEMBIAYAAN" data={pembiayaan} />
|
||||||
</Table>
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -30,56 +30,62 @@ export default function RealisasiTable({ apbdesData }: any) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper withBorder p="md" radius="md">
|
<Paper withBorder p={{ base: 'sm', sm: 'md' }} radius="md">
|
||||||
<Title order={5} mb="md">{title}</Title>
|
<Title order={5} mb="md" fz={{ base: 'sm', sm: 'md' }}>{title}</Title>
|
||||||
|
|
||||||
{allRealisasiRows.length === 0 ? (
|
{allRealisasiRows.length === 0 ? (
|
||||||
<Text fz="sm" c="dimmed" ta="center" py="md">
|
<Text fz="sm" c="dimmed" ta="center" py="md">
|
||||||
Belum ada data realisasi
|
Belum ada data realisasi
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table.ScrollContainer minWidth={300}>
|
||||||
<Table.Thead>
|
<Table verticalSpacing="xs">
|
||||||
<Table.Tr>
|
<Table.Thead>
|
||||||
<Table.Th>Uraian</Table.Th>
|
<Table.Tr>
|
||||||
<Table.Th ta="right">Realisasi (Rp)</Table.Th>
|
<Table.Th fz={{ base: 'xs', sm: 'sm' }}>Uraian</Table.Th>
|
||||||
<Table.Th ta="center">%</Table.Th>
|
<Table.Th ta="right" fz={{ base: 'xs', sm: 'sm' }}>Realisasi (Rp)</Table.Th>
|
||||||
</Table.Tr>
|
<Table.Th ta="center" fz={{ base: 'xs', sm: 'sm' }}>%</Table.Th>
|
||||||
</Table.Thead>
|
</Table.Tr>
|
||||||
<Table.Tbody>
|
</Table.Thead>
|
||||||
{allRealisasiRows.map(({ realisasi, parentItem }) => {
|
<Table.Tbody>
|
||||||
const persentase = parentItem.anggaran > 0
|
{allRealisasiRows.map(({ realisasi, parentItem }) => {
|
||||||
? (realisasi.jumlah / parentItem.anggaran) * 100
|
const persentase = parentItem.anggaran > 0
|
||||||
: 0;
|
? (realisasi.jumlah / parentItem.anggaran) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Tr key={realisasi.id}>
|
<Table.Tr key={realisasi.id}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text>{realisasi.kode || '-'} - {realisasi.keterangan || '-'}</Text>
|
<Text fz={{ base: 'xs', sm: 'sm' }} lineClamp={2}>
|
||||||
</Table.Td>
|
{realisasi.kode || '-'} - {realisasi.keterangan || '-'}
|
||||||
<Table.Td ta="right">
|
</Text>
|
||||||
<Text fw={600} c="blue">
|
</Table.Td>
|
||||||
{formatRupiah(realisasi.jumlah || 0)}
|
<Table.Td ta="right">
|
||||||
</Text>
|
<Text fw={600} c="blue" fz={{ base: 'xs', sm: 'sm' }} style={{ whiteSpace: 'nowrap' }}>
|
||||||
</Table.Td>
|
{formatRupiah(realisasi.jumlah || 0)}
|
||||||
<Table.Td ta="center">
|
</Text>
|
||||||
<Badge
|
</Table.Td>
|
||||||
color={
|
<Table.Td ta="center">
|
||||||
persentase >= 100
|
<Badge
|
||||||
? 'teal'
|
size="sm"
|
||||||
: persentase >= 60
|
variant="light"
|
||||||
? 'yellow'
|
color={
|
||||||
: 'red'
|
persentase >= 100
|
||||||
}
|
? 'teal'
|
||||||
>
|
: persentase >= 60
|
||||||
{persentase.toFixed(2)}%
|
? 'yellow'
|
||||||
</Badge>
|
: 'red'
|
||||||
</Table.Td>
|
}
|
||||||
</Table.Tr>
|
>
|
||||||
);
|
{persentase.toFixed(1)}%
|
||||||
})}
|
</Badge>
|
||||||
</Table.Tbody>
|
</Table.Td>
|
||||||
</Table>
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import "@mantine/core/styles.css";
|
import "@mantine/core/styles.css";
|
||||||
import "./globals.css";
|
import "./globals.css"; // Sisanya import di globals.css
|
||||||
|
|
||||||
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
|
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
|
||||||
import { MusicProvider } from "@/app/context/MusicContext";
|
import { MusicProvider } from "@/app/context/MusicContext";
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
MantineProvider,
|
MantineProvider,
|
||||||
createTheme,
|
createTheme,
|
||||||
mantineHtmlProps,
|
mantineHtmlProps,
|
||||||
|
// mantineHtmlProps,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Metadata, Viewport } from "next";
|
import { Metadata, Viewport } from "next";
|
||||||
import { ViewTransitions } from "next-view-transitions";
|
import { ViewTransitions } from "next-view-transitions";
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function Page() {
|
|||||||
dengan ketentuan ini, harap jangan gunakan Website.
|
dengan ketentuan ini, harap jangan gunakan Website.
|
||||||
</Text>
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
||||||
|
|||||||
Reference in New Issue
Block a user