first commit
This commit is contained in:
commit
b47f825d54
83 changed files with 10016 additions and 0 deletions
62
frontend/src/composables/useAdmin.ts
Normal file
62
frontend/src/composables/useAdmin.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { ref, computed } from "vue";
|
||||
import type { Ref, ComputedRef } from "vue";
|
||||
|
||||
const token: Ref<string | null> = ref(null);
|
||||
const isAdmin: Ref<boolean> = ref(false);
|
||||
const verifying: Ref<boolean> = ref(false);
|
||||
|
||||
async function verifyToken(t: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch("/api/auth/verify", {
|
||||
headers: { Authorization: `Bearer ${t}` },
|
||||
});
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check sessionStorage on module load
|
||||
const stored = sessionStorage.getItem("admin_token");
|
||||
if (stored) {
|
||||
verifying.value = true;
|
||||
verifyToken(stored).then((valid) => {
|
||||
if (valid) {
|
||||
token.value = stored;
|
||||
isAdmin.value = true;
|
||||
} else {
|
||||
sessionStorage.removeItem("admin_token");
|
||||
}
|
||||
verifying.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
export function useAdmin() {
|
||||
async function login(t: string): Promise<boolean> {
|
||||
verifying.value = true;
|
||||
const valid = await verifyToken(t);
|
||||
if (valid) {
|
||||
token.value = t;
|
||||
isAdmin.value = true;
|
||||
sessionStorage.setItem("admin_token", t);
|
||||
}
|
||||
verifying.value = false;
|
||||
return valid;
|
||||
}
|
||||
|
||||
function logout(): void {
|
||||
token.value = null;
|
||||
isAdmin.value = false;
|
||||
sessionStorage.removeItem("admin_token");
|
||||
}
|
||||
|
||||
const authHeaders = computed((): Record<string, string> =>
|
||||
token.value ? { Authorization: `Bearer ${token.value}` } : {}
|
||||
);
|
||||
|
||||
const authQuery: ComputedRef<string> = computed(() =>
|
||||
token.value ? `?token=${encodeURIComponent(token.value)}` : ""
|
||||
);
|
||||
|
||||
return { token, isAdmin, verifying, login, logout, authHeaders, authQuery };
|
||||
}
|
||||
42
frontend/src/composables/useAssetUpload.ts
Normal file
42
frontend/src/composables/useAssetUpload.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { ref } from "vue";
|
||||
|
||||
export function useAssetUpload() {
|
||||
const uploading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
async function upload(
|
||||
file: File,
|
||||
authHeaders: Record<string, string>
|
||||
): Promise<string | null> {
|
||||
uploading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const res = await fetch("/api/assets/", {
|
||||
method: "POST",
|
||||
headers: authHeaders,
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({ detail: "Upload failed" }));
|
||||
const detail = (body as { detail?: string }).detail ?? "Upload failed";
|
||||
error.value = detail;
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await res.json()) as { filename: string };
|
||||
return data.filename;
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Upload failed";
|
||||
return null;
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return { uploading, error, upload };
|
||||
}
|
||||
30
frontend/src/composables/useDarkMode.ts
Normal file
30
frontend/src/composables/useDarkMode.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { ref, watch } from "vue";
|
||||
|
||||
const isDark = ref(false);
|
||||
|
||||
// Initialize from localStorage or system preference
|
||||
const stored = localStorage.getItem("dark_mode");
|
||||
if (stored !== null) {
|
||||
isDark.value = stored === "true";
|
||||
} else {
|
||||
isDark.value = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
}
|
||||
|
||||
// Apply class to <html>
|
||||
function applyClass() {
|
||||
document.documentElement.classList.toggle("dark", isDark.value);
|
||||
}
|
||||
applyClass();
|
||||
|
||||
watch(isDark, () => {
|
||||
applyClass();
|
||||
localStorage.setItem("dark_mode", String(isDark.value));
|
||||
});
|
||||
|
||||
export function useDarkMode() {
|
||||
function toggle() {
|
||||
isDark.value = !isDark.value;
|
||||
}
|
||||
|
||||
return { isDark, toggle };
|
||||
}
|
||||
38
frontend/src/composables/useSegments.ts
Normal file
38
frontend/src/composables/useSegments.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { ref, onMounted } from "vue";
|
||||
import type { Segment, SiteInfo } from "@/types/segment";
|
||||
|
||||
export function useSegments() {
|
||||
const segments = ref<Segment[]>([]);
|
||||
const siteTitle = ref("Handin");
|
||||
const loading = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const [siteRes, segmentsRes] = await Promise.all([
|
||||
fetch("/api/site/"),
|
||||
fetch("/api/segments/"),
|
||||
]);
|
||||
if (!siteRes.ok) throw new Error(`Site fetch failed: ${siteRes.status}`);
|
||||
if (!segmentsRes.ok) throw new Error(`Segments fetch failed: ${segmentsRes.status}`);
|
||||
|
||||
const siteData = (await siteRes.json()) as SiteInfo;
|
||||
const segmentsData = (await segmentsRes.json()) as Segment[];
|
||||
|
||||
siteTitle.value = siteData.title;
|
||||
segments.value = segmentsData.sort((a, b) => a.sort_order - b.sort_order);
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Unknown error";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void refresh();
|
||||
});
|
||||
|
||||
return { segments, siteTitle, loading, error, refresh };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue