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:
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user