Compare commits
12 Commits
fix-respon
...
backup-stg
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b62e01556 | |||
|
|
0dabc204bc | ||
|
|
e8f8b51686 | ||
|
|
a4db3a149d | ||
|
|
fece983ac5 | ||
| 8b7eef5fee | |||
| 8b22d01e0d | |||
| dc13e37a02 | |||
| 2d2cbef29b | |||
| 8c8a96b830 | |||
| dc3eccacbf | |||
| ffe94992e5 |
19
.env
Normal file
19
.env
Normal file
@@ -0,0 +1,19 @@
|
||||
DATABASE_URL="postgresql://bip:Production_123@localhost:5433/desa-darmasaba-v0.0.1?schema=public"
|
||||
# Seafile
|
||||
SEAFILE_TOKEN=20a19f4a04032215d50ce53292e6abdd38b9f806
|
||||
SEAFILE_REPO_ID=f0e9ee4a-fd13-49a2-81c0-f253951d063a
|
||||
SEAFILE_URL=https://cld-dkr-makuro-seafile.wibudev.com
|
||||
SEAFILE_PUBLIC_SHARE_TOKEN=3a9a9ecb5e244f4da8ae
|
||||
|
||||
# Upload
|
||||
WIBU_UPLOAD_DIR=uploads
|
||||
WIBU_DOWNLOAD_DIR="./download"
|
||||
NEXT_PUBLIC_BASE_URL="http://localhost:3000"
|
||||
EMAIL_USER=nicoarya20@gmail.com
|
||||
EMAIL_PASS=hymmfpcaqzqkfgbh
|
||||
BASE_SESSION_KEY=kp9sGx91as0Kj2Ls81nAsl2Kdj13KsxP
|
||||
BASE_TOKEN_KEY=Qm82JsA92lMnKw0291mxKaaP02KjslaA
|
||||
|
||||
# BOT-TELE
|
||||
BOT_TOKEN=8498428675:AAEQwAUjTqpvgyyC5C123nP1mAxhOg12Ph0
|
||||
CHAT_ID=5251328671
|
||||
36
.env.example
Normal file
36
.env.example
Normal file
@@ -0,0 +1,36 @@
|
||||
# Database Configuration
|
||||
DATABASE_URL="postgresql://username:password@localhost:5432/desa-darmasaba?schema=public"
|
||||
|
||||
# Seafile Configuration (File Storage)
|
||||
SEAFILE_TOKEN=your_seafile_token
|
||||
SEAFILE_REPO_ID=your_seafile_repo_id
|
||||
SEAFILE_URL=https://your-seafile-instance.com
|
||||
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
|
||||
|
||||
# Upload Configuration
|
||||
WIBU_UPLOAD_DIR=uploads
|
||||
WIBU_DOWNLOAD_DIR=./download
|
||||
|
||||
# Application Configuration
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||
|
||||
# Email Configuration (for notifications/subscriptions)
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_email_app_password
|
||||
|
||||
# Session Configuration
|
||||
BASE_SESSION_KEY=your_session_key_generate_secure_random_string
|
||||
BASE_TOKEN_KEY=your_jwt_secret_key_generate_secure_random_string
|
||||
|
||||
# Telegram Bot Configuration (for notifications)
|
||||
BOT_TOKEN=your_telegram_bot_token
|
||||
CHAT_ID=your_telegram_chat_id
|
||||
|
||||
# Session Password (for iron-session)
|
||||
SESSION_PASSWORD="your_session_password_min_32_characters_long_secure"
|
||||
|
||||
# ElevenLabs API Key (for TTS features - optional)
|
||||
ELEVENLABS_API_KEY=your_elevenlabs_api_key
|
||||
|
||||
# Environment (optional, defaults to development)
|
||||
# NODE_ENV=development
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -29,7 +29,12 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env
|
||||
.env*
|
||||
# env local files (keep .env.example)
|
||||
.env.local
|
||||
.env*.local
|
||||
.env.production
|
||||
.env.development
|
||||
!.env.example
|
||||
|
||||
# QC
|
||||
QC
|
||||
@@ -52,7 +57,5 @@ next-env.d.ts
|
||||
|
||||
.github/
|
||||
|
||||
.env.*
|
||||
|
||||
*.tar.gz
|
||||
|
||||
|
||||
60
Dockerfile
Normal file
60
Dockerfile
Normal file
@@ -0,0 +1,60 @@
|
||||
# Stage 1: Build
|
||||
FROM oven/bun:1.3 AS build
|
||||
|
||||
# Install build dependencies for native modules
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json bun.lock* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
|
||||
# Use .env.example as default env for build
|
||||
RUN cp .env.example .env
|
||||
|
||||
# Generate Prisma client
|
||||
RUN bun x prisma generate
|
||||
|
||||
# Build the application frontend
|
||||
ENV NODE_ENV=production
|
||||
RUN bun run build
|
||||
|
||||
# Stage 2: Runtime
|
||||
FROM oven/bun:1.3-slim AS runtime
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
postgresql-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy necessary files from build stage
|
||||
COPY --from=build /app/package.json ./
|
||||
COPY --from=build /app/tsconfig.json ./
|
||||
COPY --from=build /app/.next ./.next
|
||||
COPY --from=build /app/public ./public
|
||||
COPY --from=build /app/src ./src
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/prisma ./prisma
|
||||
|
||||
# Expose the port
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application
|
||||
CMD ["bun", "start"]
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
serverExternalPackages: ['@elysiajs/static', 'elysia'],
|
||||
experimental: {},
|
||||
allowedDevOrigins: [
|
||||
"http://192.168.1.82:3000", // buat akses dari HP/device lain
|
||||
|
||||
@@ -211,6 +211,9 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
|
||||
@@ -402,6 +402,7 @@ export default function CreateMusik() {
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
|
||||
@@ -92,10 +92,10 @@ const MusicPlayer = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'xs', sm: 'md', md: 100 }} py="xl">
|
||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||
<Paper
|
||||
mx="auto"
|
||||
p={{ base: 'md', sm: 'xl' }}
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
bg="white"
|
||||
@@ -105,52 +105,42 @@ const MusicPlayer = () => {
|
||||
>
|
||||
<Stack gap="md">
|
||||
<BackButton />
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align={{ base: 'flex-start', sm: 'center' }}
|
||||
direction={{ base: 'column', sm: 'row' }}
|
||||
gap="md"
|
||||
mb="xl"
|
||||
mt="md"
|
||||
>
|
||||
<Group justify="space-between" mb="xl" mt={"md"}>
|
||||
<div>
|
||||
<Text fz={{ base: '24px', sm: '32px' }} fw={700} c="#0B4F78" lh={1.2}>Selamat Datang Kembali</Text>
|
||||
<Text size="sm" c="#5A6C7D">Temukan musik favorit Anda hari ini</Text>
|
||||
<Text size="32px" fw={700} c="#0B4F78">Selamat Datang Kembali</Text>
|
||||
<Text size="md" c="#5A6C7D">Temukan musik favorit Anda hari ini</Text>
|
||||
</div>
|
||||
<TextInput
|
||||
placeholder="Cari lagu..."
|
||||
leftSection={<IconSearch size={18} />}
|
||||
radius="xl"
|
||||
w={{ base: '100%', sm: 280 }}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
styles={{ input: { backgroundColor: '#fff' } }}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Group gap="md">
|
||||
<TextInput
|
||||
placeholder="Cari lagu..."
|
||||
leftSection={<IconSearch size={18} />}
|
||||
radius="xl"
|
||||
w={280}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
styles={{ input: { backgroundColor: '#fff' } }}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
<Stack gap="xl">
|
||||
<div>
|
||||
<Text size="xl" fw={700} c="#0B4F78" mb="md">Sedang Diputar</Text>
|
||||
{currentSong ? (
|
||||
<Card radius="md" p={{ base: 'md', sm: 'xl' }} shadow="md" withBorder>
|
||||
<Flex
|
||||
direction={{ base: 'column', sm: 'row' }}
|
||||
align="center"
|
||||
gap={{ base: 'md', sm: 'xl' }}
|
||||
>
|
||||
<Card radius="md" p="xl" shadow="md">
|
||||
<Group align="center" gap="xl">
|
||||
<Avatar
|
||||
src={currentSong.coverImage?.link || '/mp3-logo.png'}
|
||||
size={120}
|
||||
size={180}
|
||||
radius="md"
|
||||
/>
|
||||
<Stack gap="md" style={{ flex: 1, width: '100%' }}>
|
||||
<Box ta={{ base: 'center', sm: 'left' }}>
|
||||
<Text fz={{ base: '20px', sm: '28px' }} fw={700} c="#0B4F78" lineClamp={1}>{currentSong.judul}</Text>
|
||||
<Stack gap="md" style={{ flex: 1 }}>
|
||||
<div>
|
||||
<Text size="28px" fw={700} c="#0B4F78">{currentSong.judul}</Text>
|
||||
<Text size="lg" c="#5A6C7D">{currentSong.artis}</Text>
|
||||
{currentSong.genre && (
|
||||
<Badge mt="xs" color="#0B4F78" variant="light">{currentSong.genre}</Badge>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="xs" c="#5A6C7D" w={42}>{formatTime(currentTime)}</Text>
|
||||
<Slider
|
||||
@@ -165,7 +155,7 @@ const MusicPlayer = () => {
|
||||
<Text size="xs" c="#5A6C7D" w={42}>{formatTime(duration || 0)}</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Group>
|
||||
</Card>
|
||||
) : (
|
||||
<Card radius="md" p="xl" shadow="md">
|
||||
@@ -185,29 +175,28 @@ const MusicPlayer = () => {
|
||||
<Grid.Col span={{ base: 12, sm: 6, lg: 4 }} key={song.id}>
|
||||
<Card
|
||||
radius="md"
|
||||
p="sm"
|
||||
p="md"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
borderColor: currentSong?.id === song.id ? '#0B4F78' : 'transparent',
|
||||
backgroundColor: currentSong?.id === song.id ? '#F0F7FA' : 'white',
|
||||
border: currentSong?.id === song.id ? '2px solid #0B4F78' : '2px solid transparent',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onClick={() => playSong(song)}
|
||||
>
|
||||
<Group gap="sm" align="center" wrap="nowrap">
|
||||
<Group gap="md" align="center">
|
||||
<Avatar
|
||||
src={song.coverImage?.link || '/mp3-logo.png'}
|
||||
size={50}
|
||||
src={song.coverImage?.link || 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop'}
|
||||
size={64}
|
||||
radius="md"
|
||||
/>
|
||||
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack gap={4} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" fw={600} c="#0B4F78" truncate>{song.judul}</Text>
|
||||
<Text size="xs" c="#5A6C7D" truncate>{song.artis}</Text>
|
||||
<Text size="xs" c="#5A6C7D">{song.artis}</Text>
|
||||
<Text size="xs" c="#8A9BA8">{song.durasi}</Text>
|
||||
</Stack>
|
||||
{currentSong?.id === song.id && isPlaying && (
|
||||
<Badge color="#0B4F78" variant="filled" size="xs">Playing</Badge>
|
||||
<Badge color="#0B4F78" variant="filled">Memutar</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</Card>
|
||||
@@ -218,42 +207,34 @@ const MusicPlayer = () => {
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
|
||||
</Paper>
|
||||
|
||||
{/* Control Player Section */}
|
||||
<Paper
|
||||
mt="xl"
|
||||
mx="auto"
|
||||
p={{ base: 'md', sm: 'xl' }}
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
bg="white"
|
||||
style={{
|
||||
border: '1px solid #eaeaea',
|
||||
position: 'sticky',
|
||||
bottom: 20,
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
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">
|
||||
<Flex align="center" justify="space-between" gap="xl" h="100%">
|
||||
<Group gap="md" style={{ flex: 1 }}>
|
||||
<Avatar
|
||||
src={currentSong?.coverImage?.link || '/mp3-logo.png'}
|
||||
size={48}
|
||||
src={currentSong?.coverImage?.link || 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop'}
|
||||
size={56}
|
||||
radius="md"
|
||||
/>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{currentSong ? (
|
||||
<>
|
||||
<Text size="sm" fw={600} c="#0B4F78" truncate>{currentSong.judul}</Text>
|
||||
<Text size="xs" c="#5A6C7D" truncate>{currentSong.artis}</Text>
|
||||
<Text size="xs" c="#5A6C7D">{currentSong.artis}</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">Tidak ada lagu</Text>
|
||||
@@ -261,31 +242,29 @@ const MusicPlayer = () => {
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
{/* Controls + Progress */}
|
||||
<Stack gap="xs" style={{ flex: 2, width: '100%' }} align="center">
|
||||
<Group gap="sm">
|
||||
<Stack gap="xs" style={{ flex: 1 }} align="center">
|
||||
<Group gap="md">
|
||||
<ActionIcon
|
||||
variant={isShuffle ? 'filled' : 'subtle'}
|
||||
color="#0B4F78"
|
||||
onClick={toggleShuffleHandler}
|
||||
radius="xl"
|
||||
size={48}
|
||||
>
|
||||
{isShuffle ? <IconArrowsShuffle size={18} /> : <IconX size={18} />}
|
||||
</ActionIcon>
|
||||
<ActionIcon variant="light" color="#0B4F78" size={48} radius="xl" onClick={skipBack}>
|
||||
<ActionIcon variant="light" color="#0B4F78" size={40} radius="xl" onClick={skipBack}>
|
||||
<IconPlayerSkipBackFilled size={20} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="#0B4F78"
|
||||
size={48}
|
||||
size={56}
|
||||
radius="xl"
|
||||
onClick={togglePlayPauseHandler}
|
||||
>
|
||||
{isPlaying ? <IconPlayerPauseFilled size={26} /> : <IconPlayerPlayFilled size={26} />}
|
||||
</ActionIcon>
|
||||
<ActionIcon variant="light" color="#0B4F78" size={48} radius="xl" onClick={skipForward}>
|
||||
<ActionIcon variant="light" color="#0B4F78" size={40} radius="xl" onClick={skipForward}>
|
||||
<IconPlayerSkipForwardFilled size={20} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
@@ -293,7 +272,6 @@ const MusicPlayer = () => {
|
||||
color="#0B4F78"
|
||||
onClick={toggleRepeatHandler}
|
||||
radius="xl"
|
||||
size="md"
|
||||
>
|
||||
{isRepeat ? <IconRepeat size={18} /> : <IconRepeatOff size={18} />}
|
||||
</ActionIcon>
|
||||
@@ -312,8 +290,7 @@ const MusicPlayer = () => {
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
{/* Volume Control - Hidden on mobile, shown on md and up */}
|
||||
<Group gap="xs" style={{ flex: 1 }} justify="flex-end" visibleFrom="md">
|
||||
<Group gap="xs" style={{ flex: 1 }} justify="flex-end">
|
||||
<ActionIcon variant="subtle" color="gray" onClick={toggleMuteHandler}>
|
||||
{isMuted || volume === 0 ? <IconVolumeOff size={20} /> : <IconVolume size={20} />}
|
||||
</ActionIcon>
|
||||
|
||||
@@ -93,19 +93,28 @@ export default function FixedPlayerBar() {
|
||||
mt="md"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
top: '50%', // Menempatkan titik atas ikon di tengah layar
|
||||
left: '0px',
|
||||
transform: 'translateY(-50%)',
|
||||
transform: 'translateY(-50%)', // Menggeser ikon ke atas sebesar setengah tingginya sendiri agar benar-benar di tengah
|
||||
borderBottomRightRadius: '20px',
|
||||
borderTopRightRadius: '20px',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.2s ease',
|
||||
zIndex: 1000 // Higher z-index
|
||||
zIndex: 1
|
||||
}}
|
||||
onClick={handleRestorePlayer}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-50%) scale(1.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-50%)';
|
||||
}}
|
||||
>
|
||||
<IconMusic size={24} color="white" />
|
||||
<IconMusic size={28} color="white" />
|
||||
</Button>
|
||||
|
||||
{/* Spacer to prevent content from being hidden behind player */}
|
||||
<Box h={20} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -122,125 +131,132 @@ export default function FixedPlayerBar() {
|
||||
bottom={0}
|
||||
left={0}
|
||||
right={0}
|
||||
p={{ base: 'xs', sm: 'sm' }}
|
||||
shadow="xl"
|
||||
p="sm"
|
||||
shadow="lg"
|
||||
style={{
|
||||
zIndex: 1000,
|
||||
zIndex: 1,
|
||||
borderTop: '1px solid rgba(0,0,0,0.1)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
}}
|
||||
>
|
||||
<Flex align="center" gap={{ base: 'xs', sm: 'md' }} justify="space-between">
|
||||
<Flex align="center" gap="md" justify="space-between">
|
||||
{/* Song Info - Left */}
|
||||
<Group gap="xs" flex={{ base: 2, sm: 1 }} style={{ minWidth: 0 }} wrap="nowrap">
|
||||
<Group gap="sm" flex={1} style={{ minWidth: 0 }}>
|
||||
<Avatar
|
||||
src={currentSong.coverImage?.link || ''}
|
||||
alt={currentSong.judul}
|
||||
size={"36"}
|
||||
size={40}
|
||||
radius="sm"
|
||||
imageProps={{ loading: 'lazy' }}
|
||||
/>
|
||||
<Box style={{ minWidth: 0, flex: 1 }}>
|
||||
<Text fz={{ base: 'xs', sm: 'sm' }} fw={600} truncate>
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
<Text fz="sm" fw={600} truncate>
|
||||
{currentSong.judul}
|
||||
</Text>
|
||||
<Text fz="10px" c="dimmed" truncate>
|
||||
<Text fz="xs" c="dimmed" truncate>
|
||||
{currentSong.artis}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
{/* Controls - Center */}
|
||||
<Group gap={"xs"} flex={{ base: 1, sm: 2 }} justify="center" wrap="nowrap">
|
||||
{/* Shuffle - Desktop Only */}
|
||||
<ActionIcon
|
||||
variant={isShuffle ? 'filled' : 'subtle'}
|
||||
color={isShuffle ? '#0B4F78' : 'gray'}
|
||||
size={"md"}
|
||||
onClick={handleToggleShuffle}
|
||||
visibleFrom="sm"
|
||||
>
|
||||
<IconArrowsShuffle size={18} />
|
||||
</ActionIcon>
|
||||
{/* Controls + Progress - Center */}
|
||||
<Group gap="xs" flex={2} justify="center">
|
||||
{/* Control Buttons */}
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
variant={isShuffle ? 'filled' : 'subtle'}
|
||||
color={isShuffle ? 'blue' : 'gray'}
|
||||
size="lg"
|
||||
onClick={handleToggleShuffle}
|
||||
title="Shuffle"
|
||||
>
|
||||
<IconArrowsShuffle size={18} />
|
||||
</ActionIcon>
|
||||
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size={"md"}
|
||||
onClick={playPrev}
|
||||
>
|
||||
<IconPlayerSkipBackFilled size={20} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="lg"
|
||||
onClick={playPrev}
|
||||
title="Previous"
|
||||
>
|
||||
<IconPlayerSkipBackFilled size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="#0B4F78"
|
||||
size={"lg"}
|
||||
radius="xl"
|
||||
onClick={togglePlayPause}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<IconPlayerPauseFilled size={24} />
|
||||
) : (
|
||||
<IconPlayerPlayFilled size={24} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color={isPlaying ? 'blue' : 'gray'}
|
||||
size="xl"
|
||||
radius="xl"
|
||||
onClick={togglePlayPause}
|
||||
title={isPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<IconPlayerPauseFilled size={24} />
|
||||
) : (
|
||||
<IconPlayerPlayFilled size={24} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size={"md"}
|
||||
onClick={playNext}
|
||||
>
|
||||
<IconPlayerSkipForwardFilled size={20} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="lg"
|
||||
onClick={playNext}
|
||||
title="Next"
|
||||
>
|
||||
<IconPlayerSkipForwardFilled size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Repeat - Desktop Only */}
|
||||
<ActionIcon
|
||||
variant={isRepeat ? 'filled' : 'subtle'}
|
||||
color={isRepeat ? '#0B4F78' : 'gray'}
|
||||
size={"md"}
|
||||
onClick={toggleRepeat}
|
||||
visibleFrom="sm"
|
||||
>
|
||||
{isRepeat ? <IconRepeat size={18} /> : <IconRepeatOff size={18} />}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color={isRepeat ? 'blue' : 'gray'}
|
||||
size="lg"
|
||||
onClick={toggleRepeat}
|
||||
title={isRepeat ? 'Repeat On' : 'Repeat Off'}
|
||||
>
|
||||
{isRepeat ? <IconRepeat size={18} /> : <IconRepeatOff size={18} />}
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
{/* Progress Bar - Desktop Only */}
|
||||
<Box w={150} ml="md" visibleFrom="md">
|
||||
{/* Progress Bar - Desktop */}
|
||||
<Box w={200} display={{ base: 'none', md: 'block' }}>
|
||||
<Slider
|
||||
value={currentTime}
|
||||
max={duration || 100}
|
||||
onChange={handleSeek}
|
||||
size="xs"
|
||||
color="#0B4F78"
|
||||
size="sm"
|
||||
color="blue"
|
||||
label={(value) => formatTime(value)}
|
||||
/>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
{/* Right Controls - Volume + Close */}
|
||||
<Group gap={4} flex={1} justify="flex-end" wrap="nowrap">
|
||||
{/* Volume Control - Tablet/Desktop */}
|
||||
<Group gap="xs" flex={1} justify="flex-end">
|
||||
<Box
|
||||
onMouseEnter={() => setShowVolume(true)}
|
||||
onMouseLeave={() => setShowVolume(false)}
|
||||
pos="relative"
|
||||
visibleFrom="sm"
|
||||
>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color={isMuted ? 'red' : 'gray'}
|
||||
size="lg"
|
||||
onClick={toggleMute}
|
||||
title={isMuted ? 'Unmute' : 'Mute'}
|
||||
>
|
||||
{isMuted ? <IconVolumeOff size={18} /> : <IconVolume size={18} />}
|
||||
{isMuted ? (
|
||||
<IconVolumeOff size={18} />
|
||||
) : (
|
||||
<IconVolume size={18} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
|
||||
<Transition
|
||||
mounted={showVolume}
|
||||
transition="scale-y"
|
||||
duration={200}
|
||||
timingFunction="ease"
|
||||
>
|
||||
{(style) => (
|
||||
<Paper
|
||||
@@ -249,8 +265,8 @@ export default function FixedPlayerBar() {
|
||||
position: 'absolute',
|
||||
bottom: '100%',
|
||||
right: 0,
|
||||
marginBottom: '10px',
|
||||
padding: '10px',
|
||||
mb: 'xs',
|
||||
p: 'sm',
|
||||
zIndex: 1001,
|
||||
}}
|
||||
shadow="md"
|
||||
@@ -260,8 +276,8 @@ export default function FixedPlayerBar() {
|
||||
value={isMuted ? 0 : volume}
|
||||
max={100}
|
||||
onChange={handleVolumeChange}
|
||||
h={80}
|
||||
color="#0B4F78"
|
||||
h={100}
|
||||
color="blue"
|
||||
size="sm"
|
||||
/>
|
||||
</Paper>
|
||||
@@ -272,29 +288,30 @@ export default function FixedPlayerBar() {
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size={"md"}
|
||||
size="lg"
|
||||
onClick={handleMinimizePlayer}
|
||||
title="Minimize player"
|
||||
>
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Flex>
|
||||
|
||||
{/* Progress Bar - Mobile (Base) */}
|
||||
<Box px="xs" mt={4} hiddenFrom="md">
|
||||
{/* Progress Bar - Mobile */}
|
||||
<Box mt="xs" display={{ base: 'block', md: 'none' }}>
|
||||
<Slider
|
||||
value={currentTime}
|
||||
max={duration || 100}
|
||||
onChange={handleSeek}
|
||||
size="xs"
|
||||
color="#0B4F78"
|
||||
size="sm"
|
||||
color="blue"
|
||||
label={(value) => formatTime(value)}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Spacer to prevent content from being hidden behind player */}
|
||||
<Box h={{ base: 70, sm: 80 }} />
|
||||
<Box h={80} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Paper, Table, Title, Text } from '@mantine/core';
|
||||
import { Paper, Table, Title } from '@mantine/core';
|
||||
|
||||
function Section({ title, data }: any) {
|
||||
if (!data || data.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table.Tr bg="gray.0">
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={2}>
|
||||
<Text fw={700} fz={{ base: 'xs', sm: 'sm' }}>{title}</Text>
|
||||
<strong>{title}</strong>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
{data.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td>
|
||||
<Text fz={{ base: 'xs', sm: 'sm' }} lineClamp={2}>
|
||||
{item.kode} - {item.uraian}
|
||||
</Text>
|
||||
{item.kode} - {item.uraian}
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Text fz={{ base: 'xs', sm: 'sm' }} fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
Rp {item.anggaran.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
Rp {item.anggaran.toLocaleString('id-ID')}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
@@ -43,24 +39,22 @@ export default function PaguTable({ apbdesData }: any) {
|
||||
const pembiayaan = items.filter((i: any) => i.tipe === 'pembiayaan');
|
||||
|
||||
return (
|
||||
<Paper withBorder p={{ base: 'sm', sm: 'md' }} radius="md">
|
||||
<Title order={5} mb="md" fz={{ base: 'sm', sm: 'md' }}>{title}</Title>
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Title order={5} mb="md">{title}</Title>
|
||||
|
||||
<Table.ScrollContainer minWidth={280}>
|
||||
<Table verticalSpacing="xs">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th fz={{ base: 'xs', sm: 'sm' }}>Uraian</Table.Th>
|
||||
<Table.Th ta="right" fz={{ base: 'xs', sm: 'sm' }}>Anggaran (Rp)</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
<Section title="1) PENDAPATAN" data={pendapatan} />
|
||||
<Section title="2) BELANJA" data={belanja} />
|
||||
<Section title="3) PEMBIAYAAN" data={pembiayaan} />
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Uraian</Table.Th>
|
||||
<Table.Th ta="right">Anggaran (Rp)</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
<Section title="1) PENDAPATAN" data={pendapatan} />
|
||||
<Section title="2) BELANJA" data={belanja} />
|
||||
<Section title="3) PEMBIAYAAN" data={pembiayaan} />
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -30,62 +30,56 @@ export default function RealisasiTable({ apbdesData }: any) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper withBorder p={{ base: 'sm', sm: 'md' }} radius="md">
|
||||
<Title order={5} mb="md" fz={{ base: 'sm', sm: 'md' }}>{title}</Title>
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Title order={5} mb="md">{title}</Title>
|
||||
|
||||
{allRealisasiRows.length === 0 ? (
|
||||
<Text fz="sm" c="dimmed" ta="center" py="md">
|
||||
Belum ada data realisasi
|
||||
</Text>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={300}>
|
||||
<Table verticalSpacing="xs">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th fz={{ base: 'xs', sm: 'sm' }}>Uraian</Table.Th>
|
||||
<Table.Th ta="right" fz={{ base: 'xs', sm: 'sm' }}>Realisasi (Rp)</Table.Th>
|
||||
<Table.Th ta="center" fz={{ base: 'xs', sm: 'sm' }}>%</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{allRealisasiRows.map(({ realisasi, parentItem }) => {
|
||||
const persentase = parentItem.anggaran > 0
|
||||
? (realisasi.jumlah / parentItem.anggaran) * 100
|
||||
: 0;
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Uraian</Table.Th>
|
||||
<Table.Th ta="right">Realisasi (Rp)</Table.Th>
|
||||
<Table.Th ta="center">%</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{allRealisasiRows.map(({ realisasi, parentItem }) => {
|
||||
const persentase = parentItem.anggaran > 0
|
||||
? (realisasi.jumlah / parentItem.anggaran) * 100
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Table.Tr key={realisasi.id}>
|
||||
<Table.Td>
|
||||
<Text fz={{ base: 'xs', sm: 'sm' }} lineClamp={2}>
|
||||
{realisasi.kode || '-'} - {realisasi.keterangan || '-'}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Text fw={600} c="blue" fz={{ base: 'xs', sm: 'sm' }} style={{ whiteSpace: 'nowrap' }}>
|
||||
{formatRupiah(realisasi.jumlah || 0)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="center">
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
color={
|
||||
persentase >= 100
|
||||
? 'teal'
|
||||
: persentase >= 60
|
||||
? 'yellow'
|
||||
: 'red'
|
||||
}
|
||||
>
|
||||
{persentase.toFixed(1)}%
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
return (
|
||||
<Table.Tr key={realisasi.id}>
|
||||
<Table.Td>
|
||||
<Text>{realisasi.kode || '-'} - {realisasi.keterangan || '-'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Text fw={600} c="blue">
|
||||
{formatRupiah(realisasi.jumlah || 0)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="center">
|
||||
<Badge
|
||||
color={
|
||||
persentase >= 100
|
||||
? 'teal'
|
||||
: persentase >= 60
|
||||
? 'yellow'
|
||||
: 'red'
|
||||
}
|
||||
>
|
||||
{persentase.toFixed(2)}%
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
|
||||
@@ -99,13 +99,13 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ViewTransitions>
|
||||
<html lang="id" {...mantineHtmlProps}>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<ColorSchemeScript defaultColorScheme="light" />
|
||||
</head>
|
||||
<body>
|
||||
<html lang="id" {...mantineHtmlProps}>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<ColorSchemeScript defaultColorScheme="light" />
|
||||
</head>
|
||||
<body>
|
||||
<ViewTransitions>
|
||||
<MusicProvider>
|
||||
<MantineProvider theme={theme} defaultColorScheme="light">
|
||||
{children}
|
||||
@@ -117,8 +117,8 @@ export default function RootLayout({
|
||||
/>
|
||||
</MantineProvider>
|
||||
</MusicProvider>
|
||||
</body>
|
||||
</html>
|
||||
</ViewTransitions>
|
||||
</ViewTransitions>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user