AI 詠唱出來的第一版 UI, 如果是只有使用 desktop device only, 是沒問題, table row 的顯示方式在 mobile device 上顯示不友善, 因為建言編號沒換行, 造成版面被擠壓, 換成 div 的顯示方式, 讓 mobile deivce 針對同一筆row, 可以顯示為多行.
Key Improvements:
- Responsive Layout:
- Desktop View: Maintains a clean, professional table layout with sortable columns.
- Mobile View: Automatically switches to a card-based layout. This prevents the “squeezed” look of the table on small screens and ensures that all information (ID, subject, status, and deadline) is clearly readable.
- Unified Styling:
- The background colors for table rows and cards are now consistent across both User and Management modes to avoid visual confusion.
- Status badges and typography have been polished for better clarity.
- Mobile Fixes:
- Language Switcher: Fixed the issue where the language switcher was not clickable on mobile devices. It now uses a robust React state management with a backdrop to handle closing the menu.
- Search & Filters: Adjusted the search input and status filter buttons to be more space-efficient on smaller screens.
- Management Dashboard:
- Refactored the management view to include the same responsive card system.
- Enhanced the status filter UI with clear counts and interactive feedback.
The application has been verified to build and lint correctly, ensuring a stable experience across all devices.
異動的程式碼:
<div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
- <div className="overflow-x-auto">
+ {/* Desktop Table View */}
+ <div className="hidden md:block overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50 text-slate-500 text-sm uppercase tracking-wider font-semibold">
@@ -1170,6 +1171,72 @@ export default function App() {
</tbody>
</table>
</div>
+
+ {/* Mobile Card View */}
+ <div className="md:hidden divide-y divide-slate-100">
+ {currentItems.length > 0 ? (
+ currentItems.map((s) => {
+ const deadlineInfo = getDeadlineInfo(s);
+ let translatedDeadlineText = deadlineInfo.text;
+ if (deadlineInfo.isDueToday) {
+ translatedDeadlineText = t.dueToday;
+ } else if (deadlineInfo.isOverdue) {
+ translatedDeadlineText = `${t.overdue} ${Math.abs(getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate)))} ${t.days}`;
+ } else {
+ translatedDeadlineText = `${t.remaining} ${getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate))} ${t.days}`;
+ }
+
+ return (
+ <div
+ key={s.id}
+ onClick={() => handleSuggestionClick(s)}
+ className="p-4 hover:bg-slate-50 active:bg-slate-100 transition-colors cursor-pointer space-y-3"
+ >
+ <div className="flex justify-between items-start">
+ <div className="flex flex-col space-y-1">
+ <span className="text-[10px] font-mono text-slate-400 uppercase tracking-wider">{s.id}</span>
+ <h3 className="font-bold text-slate-900 leading-tight">{s.subject}</h3>
+ {s.isExtended && (
+ <span className="text-[10px] text-blue-500 font-bold uppercase tracking-tighter">{t.extendedOnce}</span>
+ )}
+ </div>
+ <StatusBadge status={s.status} language={language} isUserMode={true} />
+ </div>
+
+ <div className="flex justify-between items-center text-xs">
+ <div className="flex items-center text-slate-500">
+ <Building2 size={14} className="mr-1" />
+ <span>{s.department}</span>
+ </div>
+
+ <div className="text-right">
+ {s.status === 'unprocessed' ? (
+ <span className="text-slate-400 italic">{t.suggestionReceived}</span>
+ ) : (
+ <div className="flex flex-col items-end">
+ <div className={`flex items-center space-x-1 font-medium
+ ${deadlineInfo.isDueToday ? 'text-red-600' : ''}
+ ${deadlineInfo.isOverdue ? 'text-red-500 italic' : ''}
+ ${!deadlineInfo.isOverdue && !deadlineInfo.isDueToday ? 'text-slate-600' : ''}
+ `}>
+ {deadlineInfo.isDueToday || deadlineInfo.isOverdue ? <AlertTriangle size={12} /> : <Clock size={12} className="text-orange-400" />}
+ <span>{translatedDeadlineText}</span>
+ </div>
+ <span className="text-[10px] text-slate-400">{deadlineInfo.deadlineDate}</span>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+ })
+ ) : (
+ <div className="p-12 text-center text-slate-400">
+ {t.noResults}
+ </div>
+ )}
+ </div>
+ </div>
<div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-slate-500">
<div className="flex items-center space-x-4">
<p>{t.all} {sortedSuggestions.length} {t.totalResults}</p>
@@ -1192,10 +1259,9 @@ export default function App() {
</button>
</div>
</div>
- </div>
- </motion.div>
- );
- };
+ </motion.div>
+ );
+ };
const renderManagement = () => {
const t = translations[language];
@@ -1328,13 +1394,15 @@ export default function App() {
setSearchQuery(e.target.value);
setCurrentPage(1);
}}
- className="pl-9 pr-4 py-1.5 rounded-lg border border-slate-200 text-sm focus:ring-2 focus:ring-slate-800 outline-none"
+ className="pl-9 pr-4 py-1.5 rounded-lg border border-slate-200 text-sm focus:ring-2 focus:ring-slate-800 outline-none w-40 sm:w-64"
/>
- <div className="overflow-x-auto">
+
+ {/* Desktop Table View */}
+ <div className="hidden md:block overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50 text-slate-500 text-sm uppercase tracking-wider font-semibold">
@@ -1374,7 +1442,6 @@ export default function App() {
{currentItems.length > 0 ? (
currentItems.map((s) => {
const deadlineInfo = getDeadlineInfo(s);
- // Translate deadline text
let translatedDeadlineText = deadlineInfo.text;
if (deadlineInfo.isDueToday) {
translatedDeadlineText = t.dueToday;
@@ -1388,30 +1455,44 @@ export default function App() {
<tr
key={s.id}
onClick={() => handleSuggestionClick(s)}
- className="hover:bg-slate-50 transition-colors cursor-pointer border-b border-slate-50 last:border-0"
+ className="hover:bg-slate-50 transition-colors cursor-pointer group border-b border-slate-50 last:border-0"
>
- <td className="px-6 py-4 text-sm font-mono text-slate-500">{s.id}</td>
- <td className="px-6 py-4 font-medium text-slate-900">{s.subject}</td>
- <td className="px-6 py-4 text-slate-600">{s.department}</td>
- <td className="px-6 py-4">
+ <td className="px-6 py-5 text-sm font-mono text-slate-500">
+ <span>{s.id}</span>
+ </td>
+ <td className="px-6 py-5 font-medium text-slate-900">
+ <div className="flex flex-col">
+ <span>{s.subject}</span>
+ {s.isExtended && (
+ <span className="text-[10px] text-blue-500 font-bold uppercase tracking-tighter">{t.extendedOnce}</span>
+ )}
+ </div>
+ </td>
+ <td className="px-6 py-5 text-slate-600">{s.department}</td>
+ <td className="px-6 py-5">
<StatusBadge status={s.status} language={language} isUserMode={false} />
</td>
- <td className="px-6 py-4 text-right">
+ <td className="px-6 py-5 text-right">
<div className="flex flex-col items-end space-y-0.5">
{s.status === 'unprocessed' ? (
- <div className="text-slate-400 text-xs italic">
+ <div className="text-slate-400 text-sm italic">
{t.suggestionReceived}
</div>
) : (
<>
- <div className={`flex items-center justify-end space-x-1 text-xs
+ <div className={`flex items-center justify-end space-x-1
${deadlineInfo.isDueToday ? 'text-red-600 font-medium' : ''}
${deadlineInfo.isOverdue ? 'text-red-500 font-medium italic' : ''}
${!deadlineInfo.isOverdue && !deadlineInfo.isDueToday ? 'text-slate-600' : ''}
`}>
+ {!deadlineInfo.isOverdue && !deadlineInfo.isDueToday && <Clock size={16} className="text-orange-400" />}
+ {deadlineInfo.isDueToday && <AlertTriangle size={16} />}
+ {deadlineInfo.isOverdue && <AlertTriangle size={16} />}
<span>{translatedDeadlineText}</span>
</div>
- <div className="text-[10px] text-slate-400">{deadlineInfo.deadlineDate}</div>
+ <div className="text-[10px] text-slate-400">
+ {t.deadlineLabel}: {deadlineInfo.deadlineDate}
+ </div>
</>
)}
</div>
@@ -1429,27 +1510,92 @@ export default function App() {
</tbody>
</table>
</div>
- <div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-slate-500">
- <div className="flex items-center space-x-4">
- <p>{t.all} {sortedSuggestions.length} {t.totalResults}</p>
- </div>
- <div className="flex items-center space-x-4">
- <button
- onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
- disabled={currentPage === 1}
- className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
- >
- <ChevronLeft size={20} />
- </button>
- <span className="font-medium text-slate-900">{t.page} {currentPage} {t.of} {totalPages || 1} {t.pageSuffix}</span>
- <button
- onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
- disabled={currentPage === totalPages || totalPages === 0}
- className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
- >
- <ChevronRight size={20} />
- </button>
- </div>
+
+ {/* Mobile Card View */}
+ <div className="md:hidden divide-y divide-slate-100">
+ {currentItems.length > 0 ? (
+ currentItems.map((s) => {
+ const deadlineInfo = getDeadlineInfo(s);
+ let translatedDeadlineText = deadlineInfo.text;
+ if (deadlineInfo.isDueToday) {
+ translatedDeadlineText = t.dueToday;
+ } else if (deadlineInfo.isOverdue) {
+ translatedDeadlineText = `${t.overdue} ${Math.abs(getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate)))} ${t.days}`;
+ } else {
+ translatedDeadlineText = `${t.remaining} ${getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate))} ${t.days}`;
+ }
+
+ return (
+ <div
+ key={s.id}
+ onClick={() => handleSuggestionClick(s)}
+ className="p-4 hover:bg-slate-50 active:bg-slate-100 transition-colors cursor-pointer space-y-3"
+ >
+ <div className="flex justify-between items-start">
+ <div className="flex flex-col space-y-1">
+ <span className="text-[10px] font-mono text-slate-400 uppercase tracking-wider">{s.id}</span>
+ <h3 className="font-bold text-slate-900 leading-tight">{s.subject}</h3>
+ {s.isExtended && (
+ <span className="text-[10px] text-blue-500 font-bold uppercase tracking-tighter">{t.extendedOnce}</span>
+ )}
+ </div>
+ <StatusBadge status={s.status} language={language} isUserMode={false} />
+ </div>
+
+ <div className="flex justify-between items-center text-xs">
+ <div className="flex items-center text-slate-500">
+ <Building2 size={14} className="mr-1" />
+ <span>{s.department}</span>
+ </div>
+ <div className="text-right">
+ {s.status === 'unprocessed' ? (
+ <span className="text-slate-400 italic">{t.suggestionReceived}</span>
+ ) : (
+ <div className="flex flex-col items-end">
+ <div className={`flex items-center space-x-1 font-medium
+ ${deadlineInfo.isDueToday ? 'text-red-600' : ''}
+ ${deadlineInfo.isOverdue ? 'text-red-500 italic' : ''}
+ ${!deadlineInfo.isOverdue && !deadlineInfo.isDueToday ? 'text-slate-600' : ''}
+ `}>
+ {deadlineInfo.isDueToday || deadlineInfo.isOverdue ? <AlertTriangle size={12} /> : <Clock size={12} className="text-orange-400" />}
+ <span>{translatedDeadlineText}</span>
+ </div>
+ <span className="text-[10px] text-slate-400">{deadlineInfo.deadlineDate}</span>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+ })
+ ) : (
+ <div className="p-12 text-center text-slate-400">
+ {t.noResults}
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ <div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-slate-500">
+ <div className="flex items-center space-x-4">
+ <p>{t.all} {sortedSuggestions.length} {t.totalResults}</p>
+ </div>
+ <div className="flex items-center space-x-4">
+ <button
+ onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
+ disabled={currentPage === 1}
+ className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
+ >
+ <ChevronLeft size={20} />
+ </button>
+ <span className="font-medium text-slate-900">{t.page} {currentPage} {t.of} {totalPages || 1} {t.pageSuffix}</span>
+ <button
+ onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
+ disabled={currentPage === totalPages || totalPages === 0}
+ className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
+ >
+ <ChevronRight size={20} />
+ </button>
</div>
</div>