- SectionProgress: progress bar animated, badge persentase, label status, task count - SectionReport: header ikon, left accent border, TextExpandable dengan label Indonesia - SectionLink: tap langsung buka URL, ikon per domain, long press untuk hapus - SectionFile: icon container konsisten 30×30 di semua section - SectionCancel: card subtle dengan warna error, konsisten dengan visual language baru - TextExpandable: fix bug show/hide tidak muncul setelah content diupdate - Tambah 14 style class baru di Styles.ts untuk menggantikan inline style - Terapkan semua perubahan ke fitur division/task - Fix menu "Edit Tugas" di sectionTanggalTugasTask yang terpotong karena overflow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
185 lines
6.8 KiB
TypeScript
185 lines
6.8 KiB
TypeScript
import Styles from "@/constants/Styles";
|
|
import { useTheme } from "@/providers/ThemeProvider";
|
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
|
import { useState } from "react";
|
|
import { LayoutChangeEvent, Pressable, View } from "react-native";
|
|
import Text from "./Text";
|
|
|
|
type FileItem = {
|
|
name: string
|
|
extension: string
|
|
}
|
|
|
|
type Props = {
|
|
done?: boolean
|
|
title: string
|
|
dateStart: string
|
|
dateEnd: string
|
|
files?: FileItem[]
|
|
onPress?: () => void
|
|
}
|
|
|
|
const CHAR_W = 6.5
|
|
const ICON_W = 17
|
|
const PAD_H = 16
|
|
const GAP = 6
|
|
const PLUS_W = 72
|
|
|
|
function estimateChipWidth(label: string) {
|
|
return PAD_H + ICON_W + label.length * CHAR_W
|
|
}
|
|
|
|
function getVisibleChips(files: FileItem[], containerWidth: number) {
|
|
if (containerWidth === 0) return { visible: [], extra: files.length }
|
|
|
|
let used = 0
|
|
const visible: FileItem[] = []
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const label = `${files[i].name}.${files[i].extension}`
|
|
const chipW = estimateChipWidth(label)
|
|
const isLast = i === files.length - 1
|
|
const plusChipW = isLast ? 0 : PLUS_W + GAP
|
|
const gapW = visible.length > 0 ? GAP : 0
|
|
|
|
if (used + gapW + chipW + plusChipW <= containerWidth) {
|
|
visible.push(files[i])
|
|
used += gapW + chipW
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return { visible, extra: files.length - visible.length }
|
|
}
|
|
|
|
function getFileIcon(extension: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
|
const ext = extension.toLowerCase()
|
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline'
|
|
if (ext === 'pdf') return 'file-pdf-box'
|
|
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
|
|
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
|
|
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
|
|
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
|
|
return 'file-outline'
|
|
}
|
|
|
|
export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEnd, files = [], onPress }: Props) {
|
|
const { colors, activeTheme } = useTheme()
|
|
const [containerWidth, setContainerWidth] = useState(0)
|
|
|
|
const { visible, extra } = getVisibleChips(files, containerWidth)
|
|
|
|
function onChipsLayout(e: LayoutChangeEvent) {
|
|
const w = e.nativeEvent.layout.width
|
|
if (w !== containerWidth) setContainerWidth(w)
|
|
}
|
|
|
|
const dimmed = colors.dimmed.slice(0, 7)
|
|
const successColor = activeTheme === 'dark' ? '#51CF66' : colors.success
|
|
const accentColor = done === true ? successColor : dimmed + '80'
|
|
|
|
return (
|
|
<Pressable
|
|
onPress={onPress}
|
|
style={{
|
|
flexDirection: 'row',
|
|
borderRadius: 10,
|
|
overflow: 'hidden',
|
|
borderWidth: 1,
|
|
borderColor: colors.icon + '18',
|
|
backgroundColor: colors.card,
|
|
marginBottom: 10,
|
|
}}
|
|
>
|
|
{/* Accent bar kiri */}
|
|
{done !== undefined && (
|
|
<View style={{ width: 4, backgroundColor: accentColor }} />
|
|
)}
|
|
|
|
{/* Konten */}
|
|
<View style={{ flex: 1, padding: 12 }}>
|
|
|
|
{/* Judul + badge status */}
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }}>
|
|
<Text style={[Styles.textDefault, { flex: 1, marginRight: 8 }]}>{title}</Text>
|
|
{done !== undefined && (
|
|
<View style={{
|
|
backgroundColor: done ? successColor + '25' : dimmed + '18',
|
|
borderRadius: 20,
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 3,
|
|
alignSelf: 'flex-start',
|
|
}}>
|
|
<Text style={[Styles.textSmallSemiBold, { color: done ? successColor : colors.dimmed }]}>
|
|
{done ? 'Selesai' : 'Belum Selesai'}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Tanggal */}
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: files.length > 0 ? 8 : 0 }}>
|
|
<MaterialCommunityIcons name="calendar-outline" size={13} color={colors.dimmed} style={{ marginRight: 4 }} />
|
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{dateStart}</Text>
|
|
<MaterialCommunityIcons name="arrow-right" size={13} color={colors.dimmed} style={{ marginHorizontal: 4 }} />
|
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{dateEnd}</Text>
|
|
</View>
|
|
|
|
{/* Chips lampiran */}
|
|
{files.length > 0 && (
|
|
<View
|
|
style={{ flexDirection: 'row', gap: GAP, overflow: 'hidden' }}
|
|
onLayout={onChipsLayout}
|
|
>
|
|
{visible.map((file, index) => {
|
|
const label = `${file.name}.${file.extension}`
|
|
const chipW = Math.min(estimateChipWidth(label), containerWidth * 0.55)
|
|
return (
|
|
<View
|
|
key={index}
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: dimmed + '18',
|
|
borderRadius: 6,
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 3,
|
|
width: chipW,
|
|
}}
|
|
>
|
|
<MaterialCommunityIcons
|
|
name={getFileIcon(file.extension)}
|
|
size={13}
|
|
color={colors.dimmed}
|
|
style={{ marginRight: 4 }}
|
|
/>
|
|
<Text
|
|
style={[Styles.textSmallSemiBold, { color: colors.dimmed, flex: 1 }]}
|
|
numberOfLines={1}
|
|
ellipsizeMode="tail"
|
|
>
|
|
{label}
|
|
</Text>
|
|
</View>
|
|
)
|
|
})}
|
|
{extra > 0 && (
|
|
<View style={{
|
|
backgroundColor: dimmed + '18',
|
|
borderRadius: 6,
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 3,
|
|
}}>
|
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>
|
|
+{extra} lainnya
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)}
|
|
</View>
|
|
</Pressable>
|
|
)
|
|
}
|