All files / src/app/student-page/page/dashboard/components dashboardPreferences.js

93.75% Statements 45/48
88% Branches 44/50
100% Functions 13/13
97.61% Lines 41/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                                          3x   3x               3x 14x 14x 14x           3x 4x 4x 4x 4x     1x         14x 14x 6x 6x 6x 4x 6x   14x 6x   14x   1x         4x   2x     1x     4x       5x 2x       5x 5x 1x   4x   5x                                         14x 14x 14x 6x   14x 67x 14x    
/**
 * EI-2966 — Student Dashboard personalization helpers.
 *
 * Pure helpers for managing per-user dashboard widget visibility +
 * order via `localStorage`.  No backend involved — the preferences
 * are intentionally device-local so a student's choice on one device
 * doesn't override another.  (A future ticket can sync preferences
 * server-side; this MVP ships the visible UX first.)
 *
 * Storage key: `student_dashboard_preferences_v1`
 * Schema:
 *   {
 *     "hidden": ["widget_id", ...],
 *     "order": ["widget_id", ...]      // canonical order if user reorders
 *   }
 *
 * All helpers are defensive: malformed localStorage payloads, parsing
 * errors, and quota exceptions are caught and treated as "no
 * preferences set" (defaults apply).
 */
 
export const STUDENT_DASHBOARD_PREFS_KEY = "student_dashboard_preferences_v1";
 
export const DEFAULT_WIDGET_ORDER = [
    "gpa",
    "achievements",
    "distribution",
    "calendar",
    "search_teacher",
];
 
const _safeGet = () => {
    try {
        Iif (typeof window === "undefined" || !window.localStorage) return null;
        return window.localStorage.getItem(STUDENT_DASHBOARD_PREFS_KEY);
    } catch {
        return null;
    }
};
 
const _safeSet = (json) => {
    try {
        Iif (typeof window === "undefined" || !window.localStorage) return false;
        window.localStorage.setItem(STUDENT_DASHBOARD_PREFS_KEY, json);
        return true;
    } catch {
        // QuotaExceededError, SecurityError, etc. — preferences best-effort.
        return false;
    }
};
 
export function loadDashboardPreferences() {
    const raw = _safeGet();
    if (!raw) return { hidden: [], order: [] };
    try {
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== "object") return { hidden: [], order: [] };
        const hidden = Array.isArray(parsed.hidden)
            ? parsed.hidden.filter((s) => typeof s === "string" && s.length > 0)
            : [];
        const order = Array.isArray(parsed.order)
            ? parsed.order.filter((s) => typeof s === "string" && s.length > 0)
            : [];
        return { hidden, order };
    } catch {
        return { hidden: [], order: [] };
    }
}
 
export function saveDashboardPreferences(prefs) {
    const safe = {
        hidden: Array.isArray(prefs?.hidden)
            ? prefs.hidden.filter((s) => typeof s === "string" && s.length > 0)
            : [],
        order: Array.isArray(prefs?.order)
            ? prefs.order.filter((s) => typeof s === "string" && s.length > 0)
            : [],
    };
    return _safeSet(JSON.stringify(safe));
}
 
export function isWidgetHidden(widgetId, prefs) {
    if (!widgetId || !prefs || !Array.isArray(prefs.hidden)) return false;
    return prefs.hidden.includes(widgetId);
}
 
export function toggleWidgetVisibility(widgetId, prefs) {
    const hidden = new Set(Array.isArray(prefs?.hidden) ? prefs.hidden : []);
    if (hidden.has(widgetId)) {
        hidden.delete(widgetId);
    } else {
        hidden.add(widgetId);
    }
    return {
        hidden: Array.from(hidden),
        order: Array.isArray(prefs?.order) ? [...prefs.order] : [],
    };
}
 
/**
 * Compute the effective rendered order of widgets given the user's
 * personalization preferences and the canonical default order.
 *
 * Rules:
 *   - User-ordered widgets (in `prefs.order`) come first, in the
 *     order the user chose.
 *   - Widgets the user hasn't explicitly ordered come next, in
 *     `DEFAULT_WIDGET_ORDER` order — so new widgets shipped after
 *     a user set their preferences still appear.
 *   - Hidden widgets are filtered out entirely.
 *   - Unknown widget ids in `prefs.order` (e.g., stale localStorage
 *     after a widget was removed) are filtered out.
 */
export function resolveWidgetOrder(prefs, knownWidgetIds = DEFAULT_WIDGET_ORDER) {
    const known = new Set(knownWidgetIds);
    const hidden = new Set(Array.isArray(prefs?.hidden) ? prefs.hidden : []);
    const userOrdered = Array.isArray(prefs?.order)
        ? prefs.order.filter((id) => known.has(id) && !hidden.has(id))
        : [];
    const seen = new Set(userOrdered);
    const fallback = knownWidgetIds.filter((id) => !seen.has(id) && !hidden.has(id));
    return [...userOrdered, ...fallback];
}