first commit
CI / lint-and-test (push) Canceled after 0s

This commit is contained in:
Alberto-Audrix committed 2026-07-28 08:55:05 +07:00
commit 32a36cceff
444 files changed
+67186

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
import React from 'react';
import { BrainIcon } from '../icons/Icons.tsx';
interface AiInsightCardProps {
title: string;
children: React.ReactNode;
icon?: React.ReactNode;
}
const AiInsightCard: React.FC<AiInsightCardProps> = ({ title, children, icon }) => {
return (
<div className="bg-purple-50 border-l-4 border-purple-400 p-4 rounded-r-lg shadow-sm">
<div className="flex items-center mb-2">
<div className="text-purple-600 mr-3 text-xl">{icon || <BrainIcon />}</div>
<h3 className="text-lg font-bold text-purple-900">{title}</h3>
</div>
<div className="pl-9 text-sm text-purple-800/90 space-y-2">{children}</div>
</div>
);
};
export default AiInsightCard;
+132
View File
@@ -0,0 +1,132 @@
import React, { useState, useMemo } from 'react';
interface CalendarEvent {
id: string;
date: Date;
text: string;
color: string;
}
interface CalendarViewProps {
events: CalendarEvent[];
onDateSelect: (date: Date) => void;
selectedDate: Date | null;
}
const EventPill: React.FC<{ text: string; color: string }> = ({ text, color }) => (
<div className={`text-white text-[10px] px-1.5 py-0.5 rounded-full truncate ${color}`}>
{text}
</div>
);
const CalendarView: React.FC<CalendarViewProps> = ({ events, onDateSelect, selectedDate }) => {
const [currentMonth, setCurrentMonth] = useState(new Date());
const changeMonth = (amount: number) => {
setCurrentMonth((prev) => new Date(prev.getFullYear(), prev.getMonth() + amount, 1));
};
const calendarGrid = useMemo(() => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
const firstDayOfMonth = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const grid = [];
let dayCounter = 1;
for (let i = 0; i < 6; i++) {
// 6 weeks for full display
const week = [];
for (let j = 0; j < 7; j++) {
if ((i === 0 && j < firstDayOfMonth) || dayCounter > daysInMonth) {
week.push(null);
} else {
const date = new Date(year, month, dayCounter);
week.push(date);
dayCounter++;
}
}
grid.push(week);
if (dayCounter > daysInMonth) break;
}
return grid;
}, [currentMonth]);
const isSameDay = (d1: Date, d2: Date) =>
d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
return (
<div className="w-full">
<div className="flex justify-between items-center mb-4 px-2">
<button
onClick={() => changeMonth(-1)}
className="text-gray-600 hover:text-red-600 p-2 rounded-full w-10 h-10 flex items-center justify-center hover:bg-gray-100"
>
<i className="fa-solid fa-chevron-left"></i>
</button>
<h3 className="text-lg font-bold text-gray-800">
{currentMonth.toLocaleString('id-ID', { month: 'long', year: 'numeric' })}
</h3>
<button
onClick={() => changeMonth(1)}
className="text-gray-600 hover:text-red-600 p-2 rounded-full w-10 h-10 flex items-center justify-center hover:bg-gray-100"
>
<i className="fa-solid fa-chevron-right"></i>
</button>
</div>
<div className="grid grid-cols-7 gap-1 text-center text-xs font-semibold text-gray-500 mb-2">
{['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'].map((day) => (
<div key={day}>{day}</div>
))}
</div>
<div className="grid grid-cols-7 gap-1">
{calendarGrid.flat().map((date, index) => {
if (!date) {
return <div key={`empty-${index}`} className="h-24 bg-gray-50 rounded-md"></div>;
}
const isToday = isSameDay(date, new Date());
const isSelected = selectedDate ? isSameDay(date, selectedDate) : false;
const dayEvents = events.filter((e) => isSameDay(e.date, date));
return (
<div
key={date.toString()}
onClick={() => onDateSelect(date)}
className={`h-24 p-1.5 border rounded-md flex flex-col cursor-pointer transition-colors ${
isSelected
? 'bg-red-100 border-red-300'
: 'bg-white hover:bg-gray-100 border-gray-200'
}`}
>
<span
className={`font-semibold text-xs flex items-center justify-center w-6 h-6 rounded-full ${
isToday ? 'bg-red-600 text-white' : 'text-gray-700'
}`}
>
{date.getDate()}
</span>
<div className="mt-1 space-y-1 overflow-hidden">
{dayEvents.slice(0, 2).map((event) => (
<EventPill key={event.id} text={event.text} color={event.color} />
))}
{dayEvents.length > 2 && (
<div className="text-[10px] text-gray-500 font-semibold text-center">
+{dayEvents.length - 2} lagi
</div>
)}
</div>
</div>
);
})}
</div>
</div>
);
};
export default CalendarView;
+58
View File
@@ -0,0 +1,58 @@
import React from 'react';
interface FilterDropdownOption {
value: string | number | null;
label: string;
}
interface FilterDropdownProps {
label: string;
value: string | number | null;
options: FilterDropdownOption[];
onChange: (value: string | number | null) => void;
icon?: string;
disabled?: boolean;
}
const FilterDropdown: React.FC<FilterDropdownProps> = ({
label,
value,
options,
onChange,
icon,
disabled = false,
}) => {
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const val = e.target.value;
if (val === '') {
onChange(null);
} else if (!isNaN(Number(val))) {
onChange(Number(val));
} else {
onChange(val);
}
};
return (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{icon && <i className={`${icon} mr-2`}></i>}
{label}
</label>
<select
value={value === null ? '' : value}
onChange={handleChange}
disabled={disabled}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 bg-white text-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed"
>
{options.map((opt, idx) => (
<option key={idx} value={opt.value === null ? '' : opt.value}>
{opt.label}
</option>
))}
</select>
</div>
);
};
export default FilterDropdown;
+119
View File
@@ -0,0 +1,119 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Location, Coop, Floor } from '../../types';
interface LocationSelectorProps {
locations: Location[];
coops: Coop[];
floors: Floor[];
initialFloorId: string | null;
onFloorChange: (floorId: string | null) => void;
}
const LocationSelector: React.FC<LocationSelectorProps> = ({
locations,
coops,
floors,
initialFloorId,
onFloorChange,
}) => {
const [selectedLocationId, setSelectedLocationId] = useState<string>('');
const [selectedCoopId, setSelectedCoopId] = useState<string>('');
useEffect(() => {
if (initialFloorId) {
const floor = floors.find((f) => f.id === initialFloorId);
const coop = floor ? coops.find((c) => c.id === floor.coopId) : undefined;
const location = coop ? locations.find((l) => l.id === coop.locationId) : undefined;
if (location) setSelectedLocationId(location.id);
if (coop) setSelectedCoopId(coop.id);
} else {
setSelectedLocationId(locations[0]?.id || '');
}
}, [initialFloorId, locations, coops, floors]);
useEffect(() => {
if (!initialFloorId) {
const firstCoop = coops.find((c) => c.locationId === selectedLocationId);
setSelectedCoopId(firstCoop?.id || '');
}
}, [selectedLocationId, coops, initialFloorId]);
useEffect(() => {
if (!initialFloorId) {
const firstFloor = floors.find((f) => f.coopId === selectedCoopId);
onFloorChange(firstFloor?.id || null);
}
// We only want this to run when the coop changes, not when the parent's floor changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedCoopId, floors, initialFloorId]);
const availableCoops = useMemo(
() => coops.filter((c) => c.locationId === selectedLocationId),
[coops, selectedLocationId]
);
const availableFloors = useMemo(
() => floors.filter((f) => f.coopId === selectedCoopId),
[floors, selectedCoopId]
);
return (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label htmlFor="location" className="block text-sm font-medium text-gray-700 mb-1">
Lokasi
</label>
<select
id="location"
value={selectedLocationId}
onChange={(e) => setSelectedLocationId(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 bg-white"
>
{locations.map((l) => (
<option key={l.id} value={l.id}>
{l.name}
</option>
))}
</select>
</div>
<div>
<label htmlFor="coop" className="block text-sm font-medium text-gray-700 mb-1">
Kandang
</label>
<select
id="coop"
value={selectedCoopId}
onChange={(e) => setSelectedCoopId(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 bg-white"
disabled={!selectedLocationId}
>
{availableCoops.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</select>
</div>
<div>
<label htmlFor="floor" className="block text-sm font-medium text-gray-700 mb-1">
Lantai
</label>
<select
id="floor"
value={initialFloorId || ''}
onChange={(e) => onFloorChange(e.target.value || null)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 bg-white"
disabled={!selectedCoopId}
>
<option value="">-- Pilih Lantai --</option>
{availableFloors.map((f) => (
<option key={f.id} value={f.id}>
{f.name}
</option>
))}
</select>
</div>
</div>
);
};
export default LocationSelector;
+67
View File
@@ -0,0 +1,67 @@
import React from 'react';
import { useMarkdown } from '../../hooks/useMarkdown.ts';
interface MarkdownWithTocProps {
fileUrl: string;
}
const MarkdownWithToc: React.FC<MarkdownWithTocProps> = ({ fileUrl }) => {
const { markdownHtml, toc, isLoading, error } = useMarkdown(fileUrl);
const renderLayout = () => {
if (isLoading) {
return (
<div className="text-center col-span-full py-20">
<i className="fa-solid fa-spinner fa-spin text-4xl text-gray-400"></i>
<p className="mt-2 text-gray-600">Loading Document...</p>
</div>
);
}
if (error) {
return (
<div className="text-center text-red-500 col-span-full py-20">
<i className="fa-solid fa-exclamation-triangle text-4xl"></i>
<p className="mt-2 font-semibold">Could not load document</p>
<p className="text-sm">{error}</p>
</div>
);
}
return (
<>
<aside className="w-full lg:w-1/4 mb-8 lg:mb-0">
<div className="sticky top-24 self-start bg-white p-4 rounded-lg shadow-md border border-gray-200">
<h3 className="font-bold text-lg mb-3">Daftar Isi</h3>
{toc.length > 0 ? (
<ul className="space-y-2">
{toc.map((item) => (
<li key={item.id} style={{ marginLeft: `${(item.level - 1) * 1}rem` }}>
<a
href={`#${item.id}`}
className="text-gray-600 hover:text-red-600 text-sm hover:underline transition-colors"
>
{item.text}
</a>
</li>
))}
</ul>
) : (
<p className="text-sm text-gray-500">Tidak ada daftar isi.</p>
)}
</div>
</aside>
<main className="w-full lg:w-3/4 bg-white p-6 sm:p-8 md:p-12 rounded-lg shadow-lg">
<div className="prose max-w-none" dangerouslySetInnerHTML={{ __html: markdownHtml }} />
</main>
</>
);
};
return (
<div className="container mx-auto px-4 sm:px-6 lg:px-8 pb-8">
<div className="flex flex-col lg:flex-row lg:space-x-8">{renderLayout()}</div>
</div>
);
};
export default MarkdownWithToc;
File diff suppressed because it is too large. Load diff