Skip to content

Commit

Permalink
Merge branch 'develop' into 61-content-component-profile-view
Browse files Browse the repository at this point in the history
  • Loading branch information
Lev Z Király authored and Lev Z Király committed Nov 29, 2023
2 parents 70859dd + 63428f1 commit 8d57589
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 59 deletions.
51 changes: 38 additions & 13 deletions components/app-footer.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
<script setup lang="ts"></script>

<template>
<footer class="absolute inset-x-0 bottom-0 bg-surface text-on-surface">
<div class="mb-0 w-full bg-neutral-50 px-0 py-8 text-gray-900">
<footer
class="inset-x-0 bottom-0 h-7 bg-surface text-on-surface transition duration-500 hover:-translate-y-80"
>
<div class="flex items-center justify-center gap-2 border px-8 py-1.5 text-xs">
<span class="flex gap-1">
<span>&copy; {{ new Date().getUTCFullYear() }}</span>
<a class="hover:underline hover:underline-offset-2" href="https://www.oeaw.ac.at/acdh">
ACDH-CH
</a>
</span>
<span>|</span>
<NuxtLink class="hover:underline hover:underline-offset-2" href="/imprint">Imprint</NuxtLink>
</div>
<div class="mb-0 w-full bg-neutral-50 px-0 text-gray-900">
<div id="footer-full-content" class="mx-auto w-full px-4" tabindex="-1">
<div class="footer-logo-widget mb-4 flex items-center border-b pb-4 pt-2 text-sm font-semibold">
<div
class="footer-logo-widget mb-4 flex items-center border-b pb-4 pt-2 text-sm font-semibold"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
Expand Down Expand Up @@ -81,16 +95,6 @@
</div>
</div>
</div>
<div class="flex items-center justify-center gap-2 px-8 py-1.5 text-xs">
<span class="flex gap-1">
<span>&copy; {{ new Date().getUTCFullYear() }}</span>
<a class="hover:underline hover:underline-offset-2" href="https://www.oeaw.ac.at/acdh">
ACDH-CH
</a>
</span>
<span>|</span>
<NuxtLink class="hover:underline hover:underline-offset-2" href="/imprint">Imprint</NuxtLink>
</div>
</footer>
</template>

Expand Down Expand Up @@ -132,4 +136,25 @@
flex: 0 0 33.333%;
max-width: 33.333%;
}
@keyframes slide-in-out {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-20rem);
}
100% {
transform: translateY(0);
}
}
footer {
animation-name: slide-in-out;
animation-duration: 2s !important;
animation-timing-function: ease-in-out;
animation-delay: 500ms;
}
</style>
2 changes: 0 additions & 2 deletions components/app-header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ const titlestring = computed(() => {
return data.value?.projectConfig?.logo?.string;
});
function createWindowId(_item: ItemType) {
/**
* We intentionally do *not* use `item.target` for window id, because we don't want to
Expand Down
2 changes: 1 addition & 1 deletion components/app-navigation-menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ onScopeDispose(() => {
</template>
</MenubarContent>
</MenubarMenu>
<WindowListDropdown :isMobile="false" />
<WindowListDropdown :is-mobile="false" />
</Menubar>
</template>
2 changes: 1 addition & 1 deletion components/app-navigation-mobile-menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ onScopeDispose(() => {
<template>
<Sheet v-model:open="isSidepanelOpen">
<Menubar class="w-full border-none">
<WindowListDropdown :isMobile="true" />
<WindowListDropdown :is-mobile="true" />
</Menubar>
<SheetTrigger aria-label="Toggle menu" class="cursor-default">
<MenuIcon class="mx-3 my-1.5 h-5 w-5" />
Expand Down
4 changes: 2 additions & 2 deletions components/window-content.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const props = defineProps<Props>();

<template>
<GeoMapWindowContent v-if="props.item.kind === 'geo-map'" :params="props.item.params" />
<TextWindowContent v-if="props.item.kind === 'text'" :params="props.item.params" />
<ProfileWindowContent v-if="props.item.kind === 'profile'" :params="props.item.params" />
<TextWindowContent v-else-if="props.item.kind === 'text'" :params="props.item.params" />
<ProfileWindowContent v-else-if="props.item.kind === 'profile'" :params="props.item.params" />
<pre v-else>{{ props }}</pre>
</template>
6 changes: 4 additions & 2 deletions components/window-list-dropdown.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { AppWindowIcon, CheckIcon } from "lucide-vue-next";
const props = defineProps(["isMobile"]);
const props = defineProps<{
isMobile: boolean;
}>();
const router = useRouter();
const route = useRoute();
Expand Down Expand Up @@ -45,7 +47,7 @@ const { arrangement: currentArrangement, registry } = storeToRefs(windowsStore);
>
{{ item.winbox.title }}
</MenubarItem>
<template v-if="$props.isMobile === false">
<template v-if="!props.isMobile">
<MenubarSeparator />
<MenubarLabel>Arrangement</MenubarLabel>
<MenubarSeparator />
Expand Down
2 changes: 2 additions & 0 deletions components/window-manager.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const rootElement = ref<HTMLElement | null>(null);
const debouncedArrangeWindows = debounce(arrangeWindows, 150);
useResizeObserver(rootElement, debouncedArrangeWindows);
onMounted(windowsStore.restoreState);
</script>

<template>
Expand Down
7 changes: 2 additions & 5 deletions layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,14 @@ useHead({
*
* We always render the window manager in the layout, to avoid remounting the window root,
* and consequently having to manually mount/unmount every single window.
* of every single window.
*/
const isWindowManagerVisible = computed(() => {
return route.path === "/";
});
</script>

<template>
<div class="grid min-h-full grid-rows-[auto_1fr_auto] bg-neutral-50">
<div class="grid max-h-screen min-h-full grid-rows-[auto_1fr_auto] overflow-hidden bg-neutral-50">
<SkipLink :target-id="mainContentId">Skip to main content</SkipLink>

<AppHeader />
Expand All @@ -123,12 +122,10 @@ const isWindowManagerVisible = computed(() => {

<div :class="{ hidden: !isWindowManagerVisible }" class="relative isolate grid h-full w-full">
<WindowManager />
<AppFooter />
</div>
</MainContent>

<AppFooter />
<Toaster />

<RouteAnnouncer />
</div>
</template>
6 changes: 5 additions & 1 deletion pages/imprint.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ const imprint = await useFetch(String(createImprintUrl(defaultLocale, redmineId)

<template>
<div class="prose mx-auto w-full max-w-3xl py-8">
<NuxtLink class="float-right no-underline hover:underline hover:underline-offset-2" href="/">
<NuxtLink
aria-label="Navigate Back"
class="float-right no-underline hover:underline hover:underline-offset-2"
@click="useRouter().go(-1)"
>
×
</NuxtLink>
<h1>Imprint</h1>
Expand Down
151 changes: 149 additions & 2 deletions stores/use-windows-store.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { nanoid } from "nanoid";
import WinBox from "winbox";
import { z } from "zod";

import type { QueryDescription } from "@/lib/api-client";
import * as arrange from "@/utils/window-arrangement";

import { useToastsStore } from "./use-toasts-store";

const narrowScreenBreakpoint = 1024;

interface WindowItemBase {
id: string;
winbox: WinBox;
Expand Down Expand Up @@ -98,18 +103,98 @@ export const arrangements = {

export type WindowArrangement = keyof typeof arrangements;

const WindowState = z.object({
x: z.number(),
y: z.number(),
z: z.number(),
width: z.number(),
height: z.number(),
kind: z.string(),
title: z.string(),
params: z.unknown(),
});
type WindowStateInferred = z.infer<typeof WindowState>;

export const useWindowsStore = defineStore("windows", () => {
const registry = ref<WindowRegistry>(new Map());
const arrangement = ref<WindowArrangement>("smart-tile");

const router = useRouter();
const route = useRoute();

const toasts = useToastsStore();

async function initializeScreen() {
await navigateTo({
path: "/",
query: { w: btoa("[]"), a: arrangement.value },
});
}

const restoreState = async () => {
if (!route.query.w || !route.query.a) {
await initializeScreen();
return;
}

let windowStates: Array<WindowStateInferred>;
try {
const w = atob(route.query.w as string);
windowStates = JSON.parse(w) as Array<WindowStateInferred>;
} catch (e) {
toasts.addToast({
title: "RestoreState Error: JSON parse failed",
description: e instanceof Error ? e.message : "Unknown error, check console",
});
console.error(e);
await initializeScreen();
return;
}

if (!Array.isArray(windowStates)) {
toasts.addToast({
title: "RestoreState Error: Window list is not array",
description: "Window list parameter must be an array",
});
await initializeScreen();
return;
}

await nextTick();
windowStates.forEach((w) => {
try {
WindowState.parse(w);
addWindow({
title: w.title,
kind: w.kind as WindowItemKind,
params: w.params,
x: String(w.x) + "%",
y: String(w.y) + "%",
zIndex: w.z,
height: String(w.height) + "%",
width: String(w.width) + "%",
});
} catch (e) {
toasts.addToast({
title: "RestoreState Error: WindowState parse failed",
description: e instanceof Error ? e.message : "Unknown error, check console",
});
console.error(e);
}
});
setWindowArrangement(route.query.a as WindowArrangement);
};

function addWindow<Kind extends WindowItemKind>(params: {
id?: string | null;
title: string;
kind: Kind;
params: WindowItemMap[Kind]["params"];
x?: number | string; // string support added for "px" and "%" typed values
y?: number | string;
width?: number | string;
height?: number | string;
zIndex?: number;
}) {
const rootElement = document.getElementById(windowRootId);
if (rootElement == null) return;
Expand All @@ -130,8 +215,20 @@ export const useWindowsStore = defineStore("windows", () => {
const winbox = new WinBox({
id,
title,
x: "center",
y: "center",
index: params.zIndex ? params.zIndex : undefined,
x: params.x ? params.x : "center",
y: params.y ? params.y : "center",
width: params.width,
height: params.height,
onfocus() {
updateUrl();
},
onresize() {
updateUrl();
},
onmove() {
updateUrl();
},
onclose() {
registry.value.delete(id);
return false;
Expand Down Expand Up @@ -164,6 +261,11 @@ export const useWindowsStore = defineStore("windows", () => {
const viewport = rootElement.getBoundingClientRect();
const windows = Array.from(registry.value.values());

if (viewport.width < narrowScreenBreakpoint) {
arrange.maximize(viewport, windows);
return;
}

switch (arrangement.value) {
case "cascade": {
arrange.cascade(viewport, windows);
Expand All @@ -189,9 +291,54 @@ export const useWindowsStore = defineStore("windows", () => {

watch([() => registry.value.size, arrangement], () => {
arrangeWindows();
updateUrl();
});

function serializeWindowStates() {
const windowStates: Array<WindowStateInferred> = [];

const rootElement = document.getElementById(windowRootId);
if (rootElement == null) return;
const viewport = rootElement.getBoundingClientRect();

function viewportPercentageWith2DigitPrecision(x: number, dir: "height" | "width") {
return Math.floor((10000 * x) / viewport[dir]) / 100;
}

registry.value.forEach((w) => {
windowStates.push({
// @ts-expect-error Property missing in upstream types.
x: viewportPercentageWith2DigitPrecision(w.winbox.x as number, "width"),
// @ts-expect-error Property missing in upstream types.
y: viewportPercentageWith2DigitPrecision(w.winbox.y as number, "height"),
z: w.winbox.index,
// @ts-expect-error Property missing in upstream types.
width: viewportPercentageWith2DigitPrecision(w.winbox.width as number, "width"),
// @ts-expect-error Property missing in upstream types.
height: viewportPercentageWith2DigitPrecision(w.winbox.height as number, "height"),
kind: w.kind,
title: w.winbox.title,
params: w.params,
} as WindowStateInferred);
});
return windowStates;
}

function updateUrl() {
if (route.path === "/imprint") return;
const windowStates = serializeWindowStates();
// TODO: check url length, it may be too long. Note: shortest limit is 2047 (MS Edge) https://serpstat.com/blog/how-long-should-be-the-page-url-length-for-seo/
void navigateTo({
path: "/",
query: {
w: btoa(JSON.stringify(windowStates)),
a: arrangement.value,
},
});
}

return {
restoreState,
addWindow,
removeWindow,
registry,
Expand Down
1 change: 1 addition & 0 deletions styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
:root,
body,
#__nuxt {
overflow: hidden;
block-size: 100%;
}

Expand Down
Loading

0 comments on commit 8d57589

Please sign in to comment.