add feed in / out button page and also button to redirect to button page
This commit is contained in:
1 parent
07b8e2dc25
commit
b86bd7a2f4
17 files changed
+719
-28
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import type { Kandang } from '../../types/api.ts';
|
||||
|
||||
type FeedInButtonGridProps = {
|
||||
kandangs: Kandang[];
|
||||
variant?: 'default' | 'kiosk';
|
||||
};
|
||||
|
||||
type ButtonItem = {
|
||||
key: string;
|
||||
kandangName: string;
|
||||
label: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const toButtonItems = (kandangs: Kandang[]): ButtonItem[] =>
|
||||
kandangs.flatMap((kandang) =>
|
||||
(kandang.feed_in_button_urls ?? [])
|
||||
.filter((entry) => entry.url.trim())
|
||||
.map((entry, index) => ({
|
||||
key: `${kandang.id}-${index}`,
|
||||
kandangName: kandang.kandang_name,
|
||||
label: entry.label.trim(),
|
||||
url: entry.url.trim(),
|
||||
}))
|
||||
);
|
||||
|
||||
const FeedInButtonGrid: React.FC<FeedInButtonGridProps> = ({ kandangs, variant = 'default' }) => {
|
||||
const buttons = toButtonItems(kandangs);
|
||||
const isKiosk = variant === 'kiosk';
|
||||
|
||||
if (buttons.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-xl border border-dashed border-gray-300 bg-white text-center ${
|
||||
isKiosk ? 'p-10' : 'p-8'
|
||||
}`}
|
||||
>
|
||||
<i className="fa-solid fa-link-slash text-3xl text-gray-300 mb-3" />
|
||||
<p className="text-gray-600 font-medium">Belum ada URL Feed In yang dikonfigurasi.</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Atur alamat per kandang di menu Pengaturan.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`grid gap-4 ${
|
||||
isKiosk
|
||||
? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 max-w-5xl mx-auto'
|
||||
: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'
|
||||
}`}
|
||||
>
|
||||
{buttons.map((button) => {
|
||||
const title = button.label || button.kandangName;
|
||||
const subtitle = button.label ? button.kandangName : button.url;
|
||||
|
||||
return (
|
||||
<a
|
||||
key={button.key}
|
||||
href={button.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`group flex flex-col items-center justify-center rounded-2xl border-2 border-red-100 bg-white text-center shadow-sm transition-all hover:border-red-500 hover:shadow-lg hover:-translate-y-0.5 ${
|
||||
isKiosk ? 'min-h-40 p-8' : 'min-h-32 p-6'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`mb-3 flex items-center justify-center rounded-full bg-red-50 text-red-600 group-hover:bg-red-600 group-hover:text-white transition-colors ${
|
||||
isKiosk ? 'h-16 w-16 text-3xl' : 'h-12 w-12 text-2xl'
|
||||
}`}
|
||||
>
|
||||
<i className="fa-solid fa-arrow-up-right-from-square" />
|
||||
</span>
|
||||
<span className={`font-bold text-gray-800 ${isKiosk ? 'text-2xl' : 'text-lg'}`}>
|
||||
{title}
|
||||
</span>
|
||||
<span className="mt-2 text-xs text-gray-400 truncate max-w-full px-2">{subtitle}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedInButtonGrid;
|
||||
@@ -0,0 +1,149 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { api, errorMessage } from '../../services/apiClient.ts';
|
||||
import type { Kandang, Site } from '../../types/api.ts';
|
||||
import { Banner } from '../ui/Feedback.tsx';
|
||||
import FeedInButtonGrid from './FeedInButtonGrid.tsx';
|
||||
|
||||
type FeedInButtonPanelProps = {
|
||||
variant?: 'default' | 'kiosk';
|
||||
};
|
||||
|
||||
const hasConfiguredUrls = (kandang: Kandang): boolean =>
|
||||
(kandang.feed_in_button_urls ?? []).some((entry) => entry.url.trim());
|
||||
|
||||
const FeedInButtonPanel: React.FC<FeedInButtonPanelProps> = ({ variant = 'default' }) => {
|
||||
const [sites, setSites] = useState<Site[]>([]);
|
||||
const [kandangs, setKandangs] = useState<Kandang[]>([]);
|
||||
const [selectedSiteId, setSelectedSiteId] = useState<number | null>(null);
|
||||
const [selectedKandangId, setSelectedKandangId] = useState<number | 'all'>('all');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const isKiosk = variant === 'kiosk';
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const nextSites = await api.sites.list();
|
||||
if (cancelled) return;
|
||||
setSites(nextSites);
|
||||
setSelectedSiteId((prev) => {
|
||||
if (prev && nextSites.some((site) => site.id === prev)) return prev;
|
||||
return nextSites[0]?.id ?? null;
|
||||
});
|
||||
} catch (err) {
|
||||
if (!cancelled) setError(errorMessage(err));
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSiteId == null) {
|
||||
setKandangs([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const nextKandangs = await api.kandangs.list({ site_id: selectedSiteId });
|
||||
if (cancelled) return;
|
||||
setKandangs(nextKandangs);
|
||||
setSelectedKandangId('all');
|
||||
} catch (err) {
|
||||
if (!cancelled) setError(errorMessage(err));
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedSiteId]);
|
||||
|
||||
const filterableKandangs = useMemo(() => kandangs.filter(hasConfiguredUrls), [kandangs]);
|
||||
|
||||
const displayedKandangs = useMemo(() => {
|
||||
if (selectedKandangId === 'all') return filterableKandangs;
|
||||
return filterableKandangs.filter((kandang) => kandang.id === selectedKandangId);
|
||||
}, [filterableKandangs, selectedKandangId]);
|
||||
|
||||
const chipClass = (active: boolean) =>
|
||||
`px-4 py-2 rounded-full text-sm font-semibold border transition-colors ${
|
||||
active
|
||||
? 'bg-red-600 text-white border-red-600 shadow-sm'
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:border-red-300 hover:text-red-600'
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className={isKiosk ? 'max-w-5xl mx-auto space-y-6' : 'space-y-6'}>
|
||||
{error && <Banner kind="error">{error}</Banner>}
|
||||
|
||||
{(sites.length > 1 || filterableKandangs.length > 0) && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-4 space-y-4">
|
||||
{sites.length > 1 && (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<label className="text-sm font-medium text-gray-700 shrink-0" htmlFor="feed-in-site">
|
||||
Site
|
||||
</label>
|
||||
<select
|
||||
id="feed-in-site"
|
||||
value={selectedSiteId ?? ''}
|
||||
onChange={(e) => setSelectedSiteId(e.target.value ? Number(e.target.value) : null)}
|
||||
className="border border-gray-300 rounded-md px-3 py-2 text-sm w-full sm:max-w-xs focus:ring-red-500 focus:border-red-500"
|
||||
>
|
||||
{sites.map((site) => (
|
||||
<option key={site.id} value={site.id}>
|
||||
{site.site_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filterableKandangs.length > 0 && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">Kandang</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className={chipClass(selectedKandangId === 'all')}
|
||||
onClick={() => setSelectedKandangId('all')}
|
||||
>
|
||||
Semua
|
||||
</button>
|
||||
{filterableKandangs.map((kandang) => (
|
||||
<button
|
||||
key={kandang.id}
|
||||
type="button"
|
||||
className={chipClass(selectedKandangId === kandang.id)}
|
||||
onClick={() => setSelectedKandangId(kandang.id)}
|
||||
>
|
||||
{kandang.kandang_name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center text-sm text-gray-500 py-10">Memuat kandang...</div>
|
||||
) : (
|
||||
<FeedInButtonGrid kandangs={displayedKandangs} variant={variant} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedInButtonPanel;
|
||||
Reference in new issue
Block a user