testing 101

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-30 19:32:00 +08:00
parent fe47bdea3c
commit 17326b2c2e
78 changed files with 4804 additions and 1054 deletions
+12
View File
@@ -0,0 +1,12 @@
import { Spinner } from './ui/spinner'
const APP_NAME = import.meta.env.VITE_APP_NAME ?? 'STARR'
export default function AppLoadingScreen() {
return (
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-background">
<Spinner className="size-10 text-primary" />
<p className="mt-4 text-sm text-muted-foreground tracking-wide">Loading {APP_NAME}...</p>
</div>
)
}
+18 -4
View File
@@ -125,8 +125,12 @@ export function AssetPickerSheet({ open, onOpenChange, fileType, onSelect }) {
useEffect(() => {
if (!assets.length) return;
// Only request tokens for S3 assets we don't already have a URL for —
// this prevents duplicate POST /tokens when the assets list triggers
// this effect more than once per sheet open (e.g., two fetch effects
// both reacting to open mounting, producing two assets updates).
const s3Ids = assets
.filter((a) => a.storage_provider === "s3" && !a.thumbnail_url && !a.file_url)
.filter((a) => a.storage_provider === "s3" && !streamUrls[String(a.asset_id)])
.map((a) => a.asset_id);
if (!s3Ids.length) return;
@@ -135,10 +139,12 @@ export function AssetPickerSheet({ open, onOpenChange, fileType, onSelect }) {
api.post("/admin/media/tokens", { asset_ids: s3Ids })
.then(({ data }) => {
if (cancelled) return;
const tokens = data.data?.tokens ?? {};
const tokens = data.data?.tokens ?? {};
const thumbnails = data.data?.thumbnails ?? {};
const urls = {};
for (const [id, token] of Object.entries(tokens)) {
urls[id] = `${STREAM_BASE}/${token}`;
// Prefer presigned thumbnail URL (faster, direct); fall back to stream proxy
urls[id] = thumbnails[id] ?? `${STREAM_BASE}/${token}`;
}
setStreamUrls((prev) => ({ ...prev, ...urls }));
})
@@ -177,7 +183,15 @@ export function AssetPickerSheet({ open, onOpenChange, fileType, onSelect }) {
const handleSelect = (asset) => {
setSelected(asset.asset_id);
onSelect(asset);
// Pass the resolved stream/presigned URL as a second arg so callers
// (e.g. badge image picker) can use the authenticated URL directly
// rather than falling back to asset.file_url which is a private CDN
// key that the browser cannot load without S3 credentials.
const resolvedUrl = streamUrls[String(asset.asset_id)]
?? asset.thumbnail_url
?? asset.file_url
?? null;
onSelect(asset, resolvedUrl);
onOpenChange(false);
};
+2 -2
View File
@@ -13,7 +13,7 @@
* disabled? : boolean
* placeholder?: string
***********************************************************************************************************************************************************************/
import { useEffect, useRef, useState } from 'react';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import api from '@/utils/api.util';
import { Badge } from '@/components/ui/badge';
@@ -60,7 +60,7 @@ export default function GroupMultiSelect({
}, [groupsProp]);
// ── Position portal dropdown under trigger ────────────────────────────────
useEffect(() => {
useLayoutEffect(() => {
if (!open || !triggerRef.current) return;
const reposition = () => {
+89
View File
@@ -0,0 +1,89 @@
import { useState } from "react";
import { format } from "date-fns";
import { CalendarDays, Clock, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
/**
* DateTimePicker
*
* value — ISO string | null
* onChange — (isoString | null) => void
*/
export function DateTimePicker({ value, onChange, placeholder = "Pick date & time", disabled }) {
const [open, setOpen] = useState(false);
const dateValue = value ? new Date(value) : undefined;
const timeStr = dateValue
? `${String(dateValue.getHours()).padStart(2, "0")}:${String(dateValue.getMinutes()).padStart(2, "0")}`
: "00:00";
const handleDaySelect = (day) => {
if (!day) { onChange(null); return; }
const base = dateValue ?? new Date();
day.setHours(base.getHours(), base.getMinutes(), 0, 0);
onChange(day.toISOString());
};
const handleTimeChange = (e) => {
const [h, m] = e.target.value.split(":").map(Number);
const base = dateValue ? new Date(dateValue) : new Date();
base.setHours(h, m, 0, 0);
onChange(base.toISOString());
};
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
disabled={disabled}
className={cn(
"w-full justify-start text-left font-normal",
!value && "text-muted-foreground",
)}
>
<CalendarDays className="mr-2 h-4 w-4 shrink-0" />
{dateValue ? format(dateValue, "MMM d, yyyy HH:mm") : placeholder}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={dateValue}
onSelect={handleDaySelect}
disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))}
autoFocus
/>
<div className="border-t px-3 py-2.5 flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground shrink-0" />
<Label className="text-xs text-muted-foreground shrink-0 w-8">Time</Label>
<Input
type="time"
value={timeStr}
onChange={handleTimeChange}
className="h-8 flex-1"
/>
{value && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
onClick={() => { onChange(null); setOpen(false); }}
>
<X className="h-3.5 w-3.5" />
</Button>
)}
</div>
</PopoverContent>
</Popover>
);
}
+1 -1
View File
@@ -20,7 +20,7 @@ function Progress({
{...props}>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="size-full flex-1 bg-primary transition-all"
className={cn("size-full flex-1 transition-all", value >= 100 ? "bg-green-500" : "bg-primary")}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} />
</ProgressPrimitive.Root>
);