PWA widgets dashboard, lock screen, screensaver with active category

- Dashboard (/dashboard): configurable widget system with 6 widget types
  (current_tasks, category_time, today_progress, next_task, motivace, calendar_mini)
  stored in localStorage widget_config
- Lock screen (/lockscreen): fullscreen with clock, active group badge,
  up to 4 current tasks, gradient from group color, swipe/tap to unlock
- InactivityMonitor: auto-redirect to lockscreen after configurable timeout
  (only in PWA standalone/fullscreen mode)
- Settings: widget toggle switches, inactivity timeout slider, lockscreen preview
- manifest.json: added display_override ["fullscreen","standalone"] + orientation portrait

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 11:32:08 +00:00
parent f2915b79fa
commit 4ace4d5f7d
7 changed files with 686 additions and 1 deletions

View File

@@ -7,6 +7,20 @@ import { useTheme } from "@/components/ThemeProvider";
import { useTranslation, LOCALES } from "@/lib/i18n";
import type { Locale } from "@/lib/i18n";
import type { Group } from "@/lib/api";
import Link from "next/link";
type WidgetType = "current_tasks" | "category_time" | "today_progress" | "next_task" | "motivace" | "calendar_mini";
const ALL_WIDGETS: { key: WidgetType; label: string }[] = [
{ key: "current_tasks", label: "Aktualni ukoly" },
{ key: "category_time", label: "Aktivni kategorie" },
{ key: "today_progress", label: "Dnesni pokrok" },
{ key: "next_task", label: "Pristi ukol" },
{ key: "motivace", label: "Motivace" },
{ key: "calendar_mini", label: "Mini kalendar" },
];
const DEFAULT_WIDGETS: WidgetType[] = ["current_tasks", "category_time", "today_progress"];
interface GroupSetting {
from: string;
@@ -33,6 +47,12 @@ export default function SettingsPage() {
const [groupSettings, setGroupSettings] = useState<Record<string, GroupSetting>>({});
const [expandedGroup, setExpandedGroup] = useState<string | null>(null);
const [savedGroup, setSavedGroup] = useState<string | null>(null);
const [widgetEnabled, setWidgetEnabled] = useState<Record<WidgetType, boolean>>(() => {
const defaults: Record<WidgetType, boolean> = { current_tasks: true, category_time: true, today_progress: true, next_task: false, motivace: false, calendar_mini: false };
return defaults;
});
const [inactivityTimeout, setInactivityTimeout] = useState(5);
const [widgetSaved, setWidgetSaved] = useState(false);
useEffect(() => {
if (!token) {
@@ -48,6 +68,20 @@ export default function SettingsPage() {
// ignore
}
}
const savedWidgets = localStorage.getItem("widget_config");
if (savedWidgets) {
try {
const cfg = JSON.parse(savedWidgets);
if (Array.isArray(cfg.enabled)) {
const map: Record<WidgetType, boolean> = { current_tasks: false, category_time: false, today_progress: false, next_task: false, motivace: false, calendar_mini: false };
for (const w of cfg.enabled) map[w as WidgetType] = true;
setWidgetEnabled(map);
}
if (cfg.inactivityTimeout > 0) setInactivityTimeout(cfg.inactivityTimeout);
} catch {
// ignore
}
}
}
}, [token, router]);
@@ -129,6 +163,18 @@ export default function SettingsPage() {
setTimeout(() => setSavedGroup(null), 2000);
}
function toggleWidget(key: WidgetType) {
setWidgetEnabled(prev => ({ ...prev, [key]: !prev[key] }));
}
function saveWidgetConfig() {
const enabled = ALL_WIDGETS.filter(w => widgetEnabled[w.key]).map(w => w.key);
const config = { enabled, inactivityTimeout };
localStorage.setItem("widget_config", JSON.stringify(config));
setWidgetSaved(true);
setTimeout(() => setWidgetSaved(false), 2000);
}
function handleSave() {
if (typeof window !== "undefined") {
localStorage.setItem("taskteam_notifications", JSON.stringify(notifications));
@@ -285,6 +331,65 @@ export default function SettingsPage() {
</div>
</div>
{/* Widget & Lock Screen settings */}
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-2xl p-5">
<h2 className="text-sm font-semibold text-muted uppercase tracking-wide mb-4">Widgety & Zamykaci obrazovka</h2>
<div className="space-y-1">
{ALL_WIDGETS.map(w => (
<div key={w.key} className="flex items-center justify-between py-3">
<span className="text-sm font-medium">{w.label}</span>
<button
onClick={() => toggleWidget(w.key)}
className={`relative w-12 h-7 rounded-full transition-colors ${
widgetEnabled[w.key] ? "bg-blue-600" : "bg-gray-300 dark:bg-gray-600"
}`}
aria-label={w.label}
>
<div className={`absolute top-0.5 w-6 h-6 bg-white rounded-full shadow transition-transform ${
widgetEnabled[w.key] ? "translate-x-5" : "translate-x-0.5"
}`} />
</button>
</div>
))}
</div>
{/* Inactivity timeout */}
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">Neaktivita (min)</span>
<span className="text-sm text-muted">{inactivityTimeout} min</span>
</div>
<input
type="range"
min={1}
max={30}
step={1}
value={inactivityTimeout}
onChange={e => setInactivityTimeout(Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-muted mt-1">Po teto dobe neaktivity se zobrazi zamykaci obrazovka (pouze v PWA rezimu).</p>
</div>
{/* Preview lockscreen button */}
<Link
href="/lockscreen"
className="mt-4 w-full py-2.5 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm font-medium text-center block hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
>
Nahled zamykaci obrazovky
</Link>
{/* Save widget config */}
<button
onClick={saveWidgetConfig}
className={`mt-3 w-full py-2.5 rounded-xl font-medium transition-all text-sm ${
widgetSaved ? "bg-green-600 text-white" : "bg-blue-600 hover:bg-blue-700 text-white"
}`}
>
{widgetSaved ? "Ulozeno \u2713" : "Ulozit nastaveni widgetu"}
</button>
</div>
{/* Groups settings */}
{groups.length > 0 && (
<div style={{ marginTop: 8 }}>