|
5 | 5 | * Created Date: 2025-09-08 15:54:21 |
6 | 6 | * Author: 3urobeat |
7 | 7 | * |
8 | | - * Last Modified: 2026-05-16 17:58:13 |
| 8 | + * Last Modified: 2026-05-20 22:22:28 |
9 | 9 | * Modified By: 3urobeat |
10 | 10 | * |
11 | 11 | * Copyright (c) 2025 - 2026 3urobeat <https://github.com/3urobeat> |
|
19 | 19 |
|
20 | 20 | <template> |
21 | 21 |
|
22 | | - <header id="titlebar"> |
23 | | - <PhList :class="!showNavbar ? 'block' : 'opacity-0'" class="fixed z-50 cursor-pointer left-3 top-4.5 dark:text-text-dark lg:hidden block transition-opacity" size="25px" @click="showNavbar = !showNavbar"></PhList> |
24 | | - <PhCaretLeft :class="showNavbar ? 'block' : 'opacity-0'" class="fixed z-50 cursor-pointer left-3 top-4.5 dark:text-text-dark lg:hidden block transition-opacity" size="25px" @click="showNavbar = !showNavbar"></PhCaretLeft> |
25 | | - |
26 | | - <GlobalTitleBar></GlobalTitleBar> |
27 | | - </header> |
28 | | - |
29 | | - |
30 | | - <!-- Left navigation bar which offsets everything else to the right on desktop and overlays everything on mobile --> |
31 | | - <nav |
32 | | - id="navbar" |
33 | | - :class="showNavbar ? '' : 'invisible lg:visible w-0 min-w-0 opacity-0'" |
34 | | - class="fixed top-15 z-20 w-52 min-w-52 min-h-screen backdrop-blur-md lg:opacity-100 dark:text-text-dark border-x border-x-border-primary-light dark:border-x-border-primary-dark border-l-0 select-none duration-500 transition-[width,opacity,visibility]" |
35 | | - > |
36 | | - |
37 | | - <div class="absolute left-1/2 transform -translate-x-1/2 top-2 w-34"> |
38 | | - <div class="my-3"></div> <!-- Add some space above everything--> |
39 | | - |
40 | | - <NuxtLink to="/" class="group custom-navbar-link"> |
41 | | - <span class="fixed self-center mb-1 text-xl font-bold text-green-600" v-show="route.name === 'index' || route.name === 'clothing'">|</span> |
42 | | - |
43 | | - <TextOverflowAutoScroll class="ml-4"> |
44 | | - <PhHouse class="mr-2" /> {{ $t("browse") }} |
45 | | - </TextOverflowAutoScroll> |
46 | | - </NuxtLink> |
47 | | - |
48 | | - <div class="my-2 h-0.5 bg-border-secondary-light dark:bg-border-secondary-dark opacity-50"></div> <!-- Divider to give Browse more presence --> |
49 | | - |
50 | | - <NuxtLink to="/outfits" class="group custom-navbar-link"> |
51 | | - <span class="fixed self-center mb-1 text-xl font-bold text-green-600" v-show="route.name === 'outfits'">|</span> |
52 | | - |
53 | | - <TextOverflowAutoScroll class="ml-4"> |
54 | | - <PhCoatHanger class="mr-2" /> {{ $t("outfits") }} |
55 | | - </TextOverflowAutoScroll> |
56 | | - </NuxtLink> |
57 | | - <NuxtLink to="/labels" class="group custom-navbar-link"> |
58 | | - <span class="fixed self-center mb-1 text-xl font-bold text-green-600" v-show="route.name === 'labels'">|</span> |
59 | | - |
60 | | - <TextOverflowAutoScroll class="ml-4"> |
61 | | - <PhTag class="mr-2" /> {{ $t("labels") }} |
62 | | - </TextOverflowAutoScroll> |
63 | | - </NuxtLink> |
64 | | - <NuxtLink to="/settings" class="group custom-navbar-link"> |
65 | | - <span class="fixed self-center mb-1 text-xl font-bold text-green-600" v-show="route.name === 'settings'">|</span> |
66 | | - |
67 | | - <TextOverflowAutoScroll class="ml-4"> |
68 | | - <PhGear class="mr-2" /> {{ $t("settings") }} |
69 | | - </TextOverflowAutoScroll> |
70 | | - </NuxtLink> |
71 | | - </div> |
72 | | - |
73 | | - </nav> |
74 | | - |
75 | | - <!-- Footer for project details. Separated from nav container because backdrop caused positioning issues --> |
76 | | - <footer |
77 | | - :class="showNavbar ? '' : 'invisible lg:visible opacity-0'" |
78 | | - class="fixed z-20 text-nowrap bottom-0 left-0 pb-2 px-2.5 group lg:opacity-100 dark:text-text-dark select-none duration-500 transition-all" |
79 | | - > |
80 | | - <div class="flex flex-col text-sm opacity-50"> |
81 | | - <div :class="onlineVersion && onlineVersion != packagejson.version ? '' : 'hidden'" class="mb-4 px-1 py-0.5 bg-bg-embed-light dark:bg-bg-embed-dark outline-2 outline-border-secondary-light dark:outline-border-secondary-dark rounded-lg"> |
82 | | - <p class="font-semibold">{{ $t("navbarUpdateAvailable") }}</p> |
83 | | - <p>{{ $t("navbarNewVersion") }} <span class="text-green-500 font-extrabold">{{ onlineVersion }}</span></p> |
84 | | - {{ $t("navbarPatchNotesText") }} <a class="underline hover:text-gray-500" :href="'https://github.com/wardrobe-hq/wardrobe/releases/tag/' + onlineVersion" target="_blank">{{ $t("navbarPatchNotesTextLink") }}</a> |
| 22 | + <!-- Fullscreen loading page shown until server startup is complete --> |
| 23 | + <div v-if="isReady.error.value"> |
| 24 | + <div class="flex flex-col gap-8 justify-center items-center min-h-[60vh] select-none text-text-light dark:text-text-dark"> |
| 25 | + <div> |
| 26 | + <div class="w-26 -mb-1.5 inline-block"> |
| 27 | + <img src="/logo-dark.png" class="h-20 object-left object-cover hidden dark:block" /> |
| 28 | + <img src="/logo-light.png" class="h-20 object-left object-cover block dark:hidden" /> |
| 29 | + </div> |
| 30 | + |
| 31 | + <span class="text-8xl font-extrabold text-transparent bg-clip-text bg-linear-to-br from-wardrobe-blue to-wardrobe-blue/50"> |
| 32 | + Wardrobe |
| 33 | + </span> |
85 | 34 | </div> |
86 | | - |
87 | | - wardrobe v{{ packagejson.version }} |
88 | | - |
89 | | - <a class="flex w-fit items-center mt-0.5 -ml-1 rounded-xl px-2 text-gray-100 bg-gray-600 hover:bg-gray-400 hover:transition-all" href="http://github.com/wardrobe-hq/wardrobe" target="_blank"> |
90 | | - |
91 | | - <!-- GitHub logo --> |
92 | | - <svg class="mr-1" width="1em" height="1em" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"> |
93 | | - <path fill-rule="evenodd" d="M10 .333A9.911 9.911 0 0 0 6.866 19.65c.5.092.678-.215.678-.477 0-.237-.01-1.017-.014-1.845-2.757.6-3.338-1.169-3.338-1.169a2.627 2.627 0 0 0-1.1-1.451c-.9-.615.07-.6.07-.6a2.084 2.084 0 0 1 1.518 1.021 2.11 2.11 0 0 0 2.884.823c.044-.503.268-.973.63-1.325-2.2-.25-4.516-1.1-4.516-4.9A3.832 3.832 0 0 1 4.7 7.068a3.56 3.56 0 0 1 .095-2.623s.832-.266 2.726 1.016a9.409 9.409 0 0 1 4.962 0c1.89-1.282 2.717-1.016 2.717-1.016.366.83.402 1.768.1 2.623a3.827 3.827 0 0 1 1.02 2.659c0 3.807-2.319 4.644-4.525 4.889a2.366 2.366 0 0 1 .673 1.834c0 1.326-.012 2.394-.012 2.72 0 .263.18.572.681.475A9.911 9.911 0 0 0 10 .333Z" clip-rule="evenodd"/> |
94 | | - </svg> |
95 | | - |
96 | | - <span class="text-white rounded-lg text-xm" href="https://github.com/wardrobe-hq/wardrobe" target="_blank">{{ $t("navbarSourceCodeLink") }}</span> |
97 | | - |
98 | | - </a> |
99 | | - |
100 | | - <!-- Becomes visible on group hover --> |
101 | | - <div class="h-0 opacity-0 group-hover:h-10 group-hover:opacity-100 duration-500 transition-all"> |
102 | | - <p>{{ $t("navbarLicensedUnder") }} <a class="underline hover:text-gray-500" href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">AGPLv3</a></p> |
103 | | - <p>Copyright (c) 2026 <a class="underline hover:text-gray-500" href="https://github.com/3urobeat" target="_blank">3urobeat</a></p> |
104 | | - </div> |
105 | | - </div> |
106 | | - </footer> |
107 | | - |
108 | | - <!-- The main content itself, pushed to the side by the navbar - The extra lg: tags in :class fix a bg color bug when the window is resized while the navbar was open --> |
109 | | - <main |
110 | | - :class="showNavbar ? 'opacity-30 dark:opacity-70 lg:opacity-100 lg:dark:opacity-100' : ''" |
111 | | - class="fixed top-15 dark:text-text-dark transition-all duration-500" |
112 | | - @click="showNavbar = false" |
113 | | - > |
114 | | - <!-- Dummy to prevent NuxtPage button presses when the navbar is open --> |
115 | | - <div :class="showNavbar ? 'fixed min-h-screen min-w-screen opacity-0 lg:w-0 lg:h-0' : ''" class="z-50"></div> |
116 | | - |
117 | | - <!-- JS disabled warning, gets hidden by global.js. Cannot use noscript tag as it causes a Vue hydration mismatch :( --> |
118 | | - <div id="js-disabled-banner" class="fixed z-50 select-none min-h-screen min-w-screen bg-bg-field-light/90 dark:bg-bg-field-dark/90"> |
119 | | - <p class="translate-y-1/3 min-h-screen min-w-screen text-red-500 font-bold text-center">JavaScript is disabled :(<br />Please enable JavaScript to use Wardrobe.</p> |
| 35 | + <p class="text-2xl font-bold dark:text-text-dark">v{{ packagejson.version }}</p> |
| 36 | + <div class="loader"></div> |
120 | 37 | </div> |
| 38 | + </div> |
121 | 39 |
|
122 | | - <!-- Page content wrapped into a border container, used to indicate success or failure for actions --> |
123 | | - <div |
124 | | - id="color-border" |
125 | | - class="fixed left-0 lg:left-52 top-15 bottom-0 right-0 border-8 border-transparent rounded-2xl duration-500 overflow-auto" |
126 | | - > |
127 | | - <!-- Global notification component --> |
128 | | - <Notification class="fixed top-18 md:top-25 right-2 md:right-10" /> |
129 | | - |
130 | | - <div id="page-content" class="p-1 md:px-5"> |
131 | | - <NuxtPage></NuxtPage> <!-- Links to index.vue --> |
132 | | - </div> |
133 | | - </div> |
134 | | - </main> |
| 40 | + <!-- Server is ready, show app content --> |
| 41 | + <Main v-else></Main> |
135 | 42 |
|
136 | 43 | </template> |
137 | 44 |
|
138 | 45 |
|
139 | 46 | <script setup lang="ts"> |
140 | | - import { PhList, PhCaretLeft, PhHouse, PhGear, PhCoatHanger, PhTag } from "@phosphor-icons/vue"; |
141 | 47 | import packagejson from "../package.json"; |
142 | | - import type { PageProperties } from "./model/page"; |
143 | | - import TextOverflowAutoScroll from "./components/textOverflowAutoScroll.vue"; |
144 | | - import Notification from './components/notification.vue'; |
145 | | - import { closeServerSubscriptionConnection, initServerSubscriptionHandler } from "./composables/subscription"; |
146 | | - import { NotificationLevel, NotificationType, type NotificationData } from "./model/notification"; |
147 | | - import { initState, State } from "./composables/state"; |
148 | | -
|
149 | | - const route = useRoute(); |
150 | | - let changesMade = false; |
151 | | -
|
152 | | -
|
153 | | - // Refs |
154 | | - const showNavbar = ref(false); |
155 | | - const onlineVersion = ref(""); |
156 | | -
|
157 | | - // Init |
158 | | - initState(); |
159 | | - await initGlobalCache(); |
160 | | -
|
161 | | -
|
162 | | - // Handle global events |
163 | | - useNuxtApp().hook("app:user:changesMade", (val: boolean = true) => { |
164 | | - console.debug(`[DEBUG] Received changesMade = '${val}' event!`) |
165 | | - changesMade = val; |
166 | | - }); |
167 | | -
|
168 | | - useNuxtApp().hook("app:notification:action", (data: NotificationData) => { |
169 | | - if (data.type == NotificationType.RELOAD_PAGE) { |
170 | | - console.debug("[DEBUG] Got 'RELOAD_PAGE' event, reloading page..."); |
171 | | - reloadNuxtApp(); |
172 | | - } |
173 | | - }); |
174 | | -
|
175 | | - // Handle page switch |
176 | | - addRouteMiddleware("page-switch", (to, from) => { |
177 | | - if (changesMade) { |
178 | | - if (!confirm("You have unsaved changes!\nWould you still like to continue?")) { |
179 | | - return abortNavigation(); |
180 | | - } |
181 | | - } |
182 | | - changesMade = false; |
183 | | -
|
184 | | - updateGlobalSearchBar(to.meta); |
185 | | - }, { global: true }); |
| 48 | + import Main from "./main.vue"; |
186 | 49 |
|
187 | 50 |
|
188 | 51 | // Specify page information |
|
198 | 61 | script: [{ src: "/global.js" }] // Sets initial dark mode. Defined in header to fix transition load - https://stackoverflow.com/a/14416030 |
199 | 62 | }); |
200 | 63 |
|
201 | | - // Do initial page load stuff |
202 | | - updateGlobalSearchBar(useRoute().meta); |
203 | 64 |
|
204 | | - onMounted(() => { // Client side only |
205 | | - console.debug("Wardrobe mounted!"); |
206 | | - checkForUpdate(); |
207 | | - initServerSubscriptionHandler(); |
208 | | - }); |
209 | | -
|
210 | | - onUnmounted(() => { |
211 | | - closeServerSubscriptionConnection(); |
212 | | - }); |
213 | | -
|
214 | | -
|
215 | | - // Resets and toggles global search bar visibility |
216 | | - function updateGlobalSearchBar(pageProps: PageProperties) { |
217 | | - useState(State.GLOBAL_SEARCH_STRING).value = null; |
218 | | -
|
219 | | - if (pageProps && pageProps.showGlobalSearchBar) { |
220 | | - useState(State.GLOBAL_SEARCH_BAR_SHOWN).value = true; |
221 | | - } else { |
222 | | - useState(State.GLOBAL_SEARCH_BAR_SHOWN).value = false; |
223 | | - } |
224 | | - } |
225 | | -
|
226 | | - // Checks for an available update and displays a notification in the navbar |
227 | | - async function checkForUpdate() { |
228 | | - try { |
229 | | - let output = await fetch("https://raw.githubusercontent.com/wardrobe-hq/wardrobe/main/package.json"); |
230 | | - let parsed = await output.json(); |
231 | | -
|
232 | | - console.log("Successfully checked for an Update; Local: %s | Online: %s ", packagejson.version, parsed.version); |
| 65 | + // Check if server is ready once during SSR |
| 66 | + const isReady = await useFetch("/api/ping"); // Middleware will deny request if !ready |
233 | 67 |
|
234 | | - onlineVersion.value = parsed.version; |
235 | | -
|
236 | | - if (onlineVersion.value != packagejson.version) { |
237 | | - emitNotificationShowEvent({ |
238 | | - level: NotificationLevel.INFO, |
239 | | - title: $t("navbarUpdateAvailable"), |
240 | | - message: `${$t("navbarNewVersion")} ${onlineVersion.value}` |
241 | | - }); |
242 | | - } |
243 | | - } catch (err) { |
| 68 | + onMounted(async () => { // Client side only |
| 69 | + console.debug("Wardrobe mounted!"); |
244 | 70 |
|
245 | | - console.error("checkForUpdate: Failed to check GitHub repository for an available update. " + err); |
| 71 | + // If server was not ready during SSR, re-fetch API endpoint |
| 72 | + if (!isReady.data.value) { |
| 73 | + const isReadyCheckInterval = setInterval(() => { |
| 74 | + isReady.refresh(); |
| 75 | + if (isReady.data.value) clearInterval(isReadyCheckInterval); |
| 76 | + }, 1000); |
246 | 77 | } |
247 | | - } |
| 78 | + }); |
248 | 79 |
|
249 | 80 | </script> |
0 commit comments