feat: refactor news and migrate rest of useFetch to $dropFetch

This commit is contained in:
DecDuck
2025-03-14 13:12:04 +11:00
parent bd1cb67cd0
commit 1de9ebdfa5
23 changed files with 299 additions and 297 deletions
+10 -4
View File
@@ -46,16 +46,22 @@ const article = defineModel<Article | undefined>();
const deleteLoading = ref(false); const deleteLoading = ref(false);
const router = useRouter(); const router = useRouter();
const news = useNews(); const news = useNews();
if (!news.value) {
news.value = await fetchNews();
}
async function deleteArticle() { async function deleteArticle() {
try { try {
if (!article.value) return; if (!article.value || !news.value) return;
deleteLoading.value = true; deleteLoading.value = true;
await news.remove(article.value.id); await $dropFetch(`/api/v1/admin/news/${article.value.id}`, { method: "DELETE" });
const index = news.value.findIndex((e) => e.id == article.value?.id);
news.value.splice(index, 1);
article.value = undefined; article.value = undefined;
await router.push('/news'); router.push("/news");
} catch (e: any) { } catch (e: any) {
createModal( createModal(
ModalType.Notification, ModalType.Notification,
@@ -69,4 +75,4 @@ async function deleteArticle() {
deleteLoading.value = false; deleteLoading.value = false;
} }
} }
</script> </script>
@@ -4,13 +4,13 @@
<button <button
v-if="user?.admin" v-if="user?.admin"
@click="modalOpen = !modalOpen" @click="modalOpen = !modalOpen"
class="inline-flex items-center gap-x-2 px-4 py-2 rounded-lg bg-blue-600 text-white font-semibold font-display shadow-sm transition-all duration-200 hover:bg-blue-500 hover:scale-105 hover:shadow-blue-500/25 hover:shadow-lg active:scale-95" class="transition inline-flex w-full items-center px-4 gap-x-2 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 font-semibold text-sm shadow-sm"
> >
<PlusIcon <PlusIcon
class="h-5 w-5 transition-transform duration-200" class="h-5 w-5 transition-transform duration-200"
:class="{ 'rotate-90': modalOpen }" :class="{ 'rotate-90': modalOpen }"
/> />
<span>New Article</span> <span>New article</span>
</button> </button>
<ModalTemplate size-class="sm:max-w-[80vw]" v-model="modalOpen"> <ModalTemplate size-class="sm:max-w-[80vw]" v-model="modalOpen">
@@ -207,14 +207,16 @@ import {
XCircleIcon, XCircleIcon,
XMarkIcon, XMarkIcon,
} from "@heroicons/vue/24/solid"; } from "@heroicons/vue/24/solid";
import type { Article } from "@prisma/client";
import { micromark } from "micromark"; import { micromark } from "micromark";
import type { SerializeObject } from "nitropack/types";
const emit = defineEmits<{ const news = useNews();
refresh: []; if(!news.value){
}>(); news.value = await fetchNews();
}
const user = useUser(); const user = useUser();
const news = useNews();
const modalOpen = ref(false); const modalOpen = ref(false);
const loading = ref(false); const loading = ref(false);
@@ -348,11 +350,13 @@ async function createArticle() {
formData.append("content", newArticle.value.content); formData.append("content", newArticle.value.content);
formData.append("tags", JSON.stringify(newArticle.value.tags)); formData.append("tags", JSON.stringify(newArticle.value.tags));
await $dropFetch("/api/v1/admin/news", { const createdArticle = await $dropFetch("/api/v1/admin/news", {
method: "POST", method: "POST",
body: formData, body: formData,
}); });
news.value?.push(createdArticle);
// Reset form // Reset form
newArticle.value = { newArticle.value = {
title: "", title: "",
@@ -361,8 +365,6 @@ async function createArticle() {
tags: [], tags: [],
}; };
emit("refresh");
modalOpen.value = false; modalOpen.value = false;
} catch (e) { } catch (e) {
error.value = (e as any)?.statusMessage ?? "An unknown error occured."; error.value = (e as any)?.statusMessage ?? "An unknown error occured.";
+9 -7
View File
@@ -116,19 +116,21 @@ import { ref, computed } from "vue";
import { MagnifyingGlassIcon } from "@heroicons/vue/24/solid"; import { MagnifyingGlassIcon } from "@heroicons/vue/24/solid";
import { micromark } from "micromark"; import { micromark } from "micromark";
const news = useNews();
if(!news.value){
news.value = await fetchNews();
}
const route = useRoute(); const route = useRoute();
const searchQuery = ref(""); const searchQuery = ref("");
const dateFilter = ref("all"); const dateFilter = ref("all");
const selectedTags = ref<string[]>([]); const selectedTags = ref<string[]>([]);
const { data: articles, refresh: refreshArticles } = await useNews().getAll();
defineExpose({ refresh: refreshArticles });
// Get unique tags from all articles // Get unique tags from all articles
const availableTags = computed(() => { const availableTags = computed(() => {
if (!articles.value) return []; if (!news.value) return [];
const tags = new Set<string>(); const tags = new Set<string>();
articles.value.forEach((article) => { news.value.forEach((article) => {
article.tags.forEach((tag) => tags.add(tag.name)); article.tags.forEach((tag) => tags.add(tag.name));
}); });
return Array.from(tags); return Array.from(tags);
@@ -159,10 +161,10 @@ const formatExcerpt = (excerpt: string) => {
}; };
const filteredArticles = computed(() => { const filteredArticles = computed(() => {
if (!articles.value) return []; if (!news.value) return [];
// filter articles based on search, date, and tags // filter articles based on search, date, and tags
return articles.value.filter((article) => { return news.value.filter((article) => {
const matchesSearch = const matchesSearch =
article.title.toLowerCase().includes(searchQuery.value.toLowerCase()) || article.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
article.description article.description
+2 -8
View File
@@ -9,10 +9,7 @@ export const useCollections = async () => {
// @ts-expect-error // @ts-expect-error
const state = useState<FullCollection[]>("collections", () => undefined); const state = useState<FullCollection[]>("collections", () => undefined);
if (state.value === undefined) { if (state.value === undefined) {
const headers = useRequestHeaders(["cookie"]); state.value = await $dropFetch<FullCollection[]>("/api/v1/collection");
state.value = await $dropFetch<FullCollection[]>("/api/v1/collection", {
headers,
});
} }
return state; return state;
@@ -41,8 +38,5 @@ export const useLibrary = async () => {
export async function refreshLibrary() { export async function refreshLibrary() {
const state = useState<FullCollection>("library"); const state = useState<FullCollection>("library");
const headers = useRequestHeaders(["cookie"]); state.value = await $dropFetch<FullCollection>("/api/v1/collection/default");
state.value = await $dropFetch<FullCollection>("/api/v1/collection/default", {
headers,
});
} }
+34 -29
View File
@@ -1,35 +1,40 @@
export const useNews = () => { import type { Article } from "@prisma/client";
const getAll = async (options?: { import type { SerializeObject } from "nitropack";
limit?: number;
skip?: number;
orderBy?: "asc" | "desc";
tags?: string[];
search?: string;
}) => {
const query = new URLSearchParams();
if (options?.limit) query.set("limit", options.limit.toString()); export const useNews = () =>
if (options?.skip) query.set("skip", options.skip.toString()); useState<
if (options?.orderBy) query.set("order", options.orderBy); | Array<
if (options?.tags?.length) query.set("tags", options.tags.join(",")); SerializeObject<
if (options?.search) query.set("search", options.search); Article & {
tags: Array<{ id: string; name: string }>;
author: { displayName: string; id: string } | null;
}
>
>
| undefined
>("news", () => undefined);
return await useFetch(`/api/v1/news?${query.toString()}`); export const fetchNews = async (options?: {
}; limit?: number;
skip?: number;
orderBy?: "asc" | "desc";
tags?: string[];
search?: string;
}) => {
const query = new URLSearchParams();
const getById = async (id: string) => { if (options?.limit) query.set("limit", options.limit.toString());
return await useFetch(`/api/v1/news/${id}`); if (options?.skip) query.set("skip", options.skip.toString());
}; if (options?.orderBy) query.set("order", options.orderBy);
if (options?.tags?.length) query.set("tags", options.tags.join(","));
if (options?.search) query.set("search", options.search);
const remove = async (id: string) => { const news = useNews();
return await $dropFetch(`/api/v1/admin/news/${id}`, {
method: "DELETE",
});
};
return { // @ts-ignore
getAll, const newValue = await $dropFetch(`/api/v1/news?${query.toString()}`);
getById,
remove, news.value = newValue;
};
return newValue;
}; };
+5 -1
View File
@@ -31,7 +31,11 @@ export const $dropFetch: DropFetch = async (request, opts) => {
if (!getCurrentInstance()?.proxy) { if (!getCurrentInstance()?.proxy) {
return (await $fetch(request, opts)) as any; return (await $fetch(request, opts)) as any;
} }
const { data, error } = await useFetch(request, opts as any); const headers = useRequestHeaders(["cookie"]);
const { data, error } = await useFetch(request, {
...opts,
headers: { ...opts?.headers, ...headers },
} as any);
if (error.value) throw error.value; if (error.value) throw error.value;
return data.value as any; return data.value as any;
}; };
+1 -2
View File
@@ -6,11 +6,10 @@ import type { User } from "@prisma/client";
export const useUser = () => useState<User | undefined | null>(undefined); export const useUser = () => useState<User | undefined | null>(undefined);
export const updateUser = async () => { export const updateUser = async () => {
const headers = useRequestHeaders(["cookie"]);
const user = useUser(); const user = useUser();
if (user.value === null) return; if (user.value === null) return;
// SSR calls have to be after uses // SSR calls have to be after uses
user.value = await $dropFetch<User | null>("/api/v1/user", { headers }); user.value = await $dropFetch<User | null>("/api/v1/user");
}; };
+1 -2
View File
@@ -176,6 +176,5 @@ useHead({
title: "Home", title: "Home",
}); });
const headers = useRequestHeaders(["cookie"]); const libraryState = await $dropFetch("/api/v1/admin/library");
const libraryState = await $dropFetch("/api/v1/admin/library", { headers });
</script> </script>
+1 -5
View File
@@ -551,13 +551,9 @@ definePageMeta({
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const headers = useRequestHeaders(["cookie"]);
const gameId = route.params.id.toString(); const gameId = route.params.id.toString();
const versions = await $dropFetch( const versions = await $dropFetch(
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`, `/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`
{
headers,
}
); );
const currentlySelectedVersion = ref(-1); const currentlySelectedVersion = ref(-1);
const versionSettings = ref<{ const versionSettings = ref<{
+6 -7
View File
@@ -321,7 +321,10 @@
{{ item.delta ? "Upgrade mode" : "" }} {{ item.delta ? "Upgrade mode" : "" }}
</div> </div>
<div class="inline-flex items-center gap-x-2"> <div class="inline-flex items-center gap-x-2">
<component :is="PLATFORM_ICONS[item.platform]" class="size-6 text-blue-600" /> <component
:is="PLATFORM_ICONS[item.platform]"
class="size-6 text-blue-600"
/>
<Bars3Icon class="cursor-move w-6 h-6 text-zinc-400 handle" /> <Bars3Icon class="cursor-move w-6 h-6 text-zinc-400 handle" />
<button @click="() => deleteVersion(item.versionName)"> <button @click="() => deleteVersion(item.versionName)">
<TrashIcon class="w-5 h-5 text-red-600" /> <TrashIcon class="w-5 h-5 text-red-600" />
@@ -345,7 +348,7 @@
:options="{ id: game.id }" :options="{ id: game.id }"
accept="image/*" accept="image/*"
endpoint="/api/v1/admin/game/image" endpoint="/api/v1/admin/game/image"
@upload="(result) => uploadAfterImageUpload(result)" @upload="(result: Game) => uploadAfterImageUpload(result)"
/> />
<ModalTemplate v-model="showAddCarouselModal"> <ModalTemplate v-model="showAddCarouselModal">
<template #default> <template #default>
@@ -529,12 +532,8 @@ const mobileShowFinalDescription = ref(true);
const route = useRoute(); const route = useRoute();
const gameId = route.params.id.toString(); const gameId = route.params.id.toString();
const headers = useRequestHeaders(["cookie"]);
const { game: rawGame, unimportedVersions } = await $dropFetch( const { game: rawGame, unimportedVersions } = await $dropFetch(
`/api/v1/admin/game?id=${encodeURIComponent(gameId)}`, `/api/v1/admin/game?id=${encodeURIComponent(gameId)}`
{
headers,
}
); );
const game = ref(rawGame); const game = ref(rawGame);
+1 -2
View File
@@ -157,8 +157,7 @@ definePageMeta({
layout: "admin", layout: "admin",
}); });
const headers = useRequestHeaders(["cookie"]); const games = await $dropFetch("/api/v1/admin/import/game");
const games = await $dropFetch("/api/v1/admin/import/game", { headers });
const currentlySelectedGame = ref(-1); const currentlySelectedGame = ref(-1);
const gameSearchResultsLoading = ref(false); const gameSearchResultsLoading = ref(false);
+1 -2
View File
@@ -179,8 +179,7 @@ useHead({
const searchQuery = ref(""); const searchQuery = ref("");
const headers = useRequestHeaders(["cookie"]); const libraryState = await $dropFetch("/api/v1/admin/library");
const libraryState = await $dropFetch("/api/v1/admin/library", { headers });
const libraryGames = ref( const libraryGames = ref(
libraryState.games.map((e) => { libraryState.games.map((e) => {
const noVersions = e.status.noVersions; const noVersions = e.status.noVersions;
+1 -4
View File
@@ -110,10 +110,7 @@ definePageMeta({
layout: "admin", layout: "admin",
}); });
const headers = useRequestHeaders(["cookie"]); const enabledMechanisms = await $dropFetch("/api/v1/admin/auth");
const enabledMechanisms = await $dropFetch("/api/v1/admin/auth", {
headers,
});
const authenticationMechanisms: Array<{ const authenticationMechanisms: Array<{
name: string; name: string;
+3 -5
View File
@@ -391,12 +391,10 @@ useHead({
title: "Simple authentication", title: "Simple authentication",
}); });
const headers = useRequestHeaders(["cookie"]); const data = await $dropFetch<Array<SerializeObject<Invitation>>>(
const { data } = await useFetch<Array<SerializeObject<Invitation>>>( "/api/v1/admin/auth/invitation"
"/api/v1/admin/auth/invitation",
{ headers }
); );
const invitations = ref(data.value ?? []); const invitations = ref(data ?? []);
const generateInvitationUrl = (id: string) => const generateInvitationUrl = (id: string) =>
`${window.location.protocol}//${window.location.host}/register?id=${id}`; `${window.location.protocol}//${window.location.host}/register?id=${id}`;
+1 -2
View File
@@ -106,6 +106,5 @@ definePageMeta({
layout: "admin", layout: "admin",
}); });
const headers = useRequestHeaders(["cookie"]); const users = await $dropFetch("/api/v1/admin/users");
const { data: users } = await useFetch("/api/v1/admin/users", { headers });
</script> </script>
+6 -24
View File
@@ -47,7 +47,7 @@
</div> </div>
</div> </div>
<main <main
v-else-if="clientData.data.value" v-else-if="clientData"
class="mx-auto grid lg:grid-cols-2 max-w-md lg:max-w-none min-h-full place-items-center w-full gap-4 px-6 py-12 sm:py-32 lg:px-8" class="mx-auto grid lg:grid-cols-2 max-w-md lg:max-w-none min-h-full place-items-center w-full gap-4 px-6 py-12 sm:py-32 lg:px-8"
> >
<div> <div>
@@ -58,7 +58,7 @@
Authorize client? Authorize client?
</h1> </h1>
<p class="mt-6 text-base leading-7 text-zinc-400"> <p class="mt-6 text-base leading-7 text-zinc-400">
"{{ clientData.data.value.name }}" has requested access to your Drop "{{ clientData.name }}" has requested access to your Drop
account. account.
</p> </p>
<div <div
@@ -94,8 +94,8 @@
<p <p
class="mt-6 font-semibold font-display text-lg leading-8 text-zinc-100" class="mt-6 font-semibold font-display text-lg leading-8 text-zinc-100"
> >
Accepting this request will allow "{{ clientData.data.value.name }}" Accepting this request will allow "{{ clientData.name }}"
on "{{ clientData.data.value.platform }}" to: on "{{ clientData.platform }}" to:
</p> </p>
</div> </div>
<div class="mt-8 max-w-2xl sm:mt-12 lg:mt-14"> <div class="mt-8 max-w-2xl sm:mt-12 lg:mt-14">
@@ -132,22 +132,6 @@
</div> </div>
</div> </div>
</main> </main>
<main
v-else-if="clientData.error.value != undefined"
class="grid min-h-full w-full place-items-center px-6 py-24 sm:py-32 lg:px-8"
>
<div class="text-center">
<p class="text-base font-semibold text-blue-600">400</p>
<h1
class="mt-4 text-3xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl"
>
Invalid or expired request
</h1>
<p class="mt-6 text-base leading-7 text-zinc-400">
Unfortunately, we couldn't load the authorization request.
</p>
</div>
</main>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -164,10 +148,8 @@ import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/solid";
const route = useRoute(); const route = useRoute();
const clientId = route.params.id; const clientId = route.params.id;
const headers = useRequestHeaders(["cookie"]); const clientData = await $dropFetch(
const clientData = await useFetch( `/api/v1/client/auth/callback?id=${clientId}`
`/api/v1/client/auth/callback?id=${clientId}`,
{ headers }
); );
const completed = ref(false); const completed = ref(false);
+15 -8
View File
@@ -38,7 +38,9 @@
@click="() => (currentlyDeleting = collection)" @click="() => (currentlyDeleting = collection)"
class="group px-3 ml-[2px] bg-zinc-800/50 hover:bg-zinc-800 group" class="group px-3 ml-[2px] bg-zinc-800/50 hover:bg-zinc-800 group"
> >
<TrashIcon class="transition-all size-5 text-zinc-400 group-hover:text-red-400 group-hover:rotate-[8deg]" /> <TrashIcon
class="transition-all size-5 text-zinc-400 group-hover:text-red-400 group-hover:rotate-[8deg]"
/>
</button> </button>
</div> </div>
@@ -48,10 +50,16 @@
@click="collectionCreateOpen = true" @click="collectionCreateOpen = true"
class="group flex flex-row rounded-lg overflow-hidden transition-all duration-200 text-left w-full hover:scale-105" class="group flex flex-row rounded-lg overflow-hidden transition-all duration-200 text-left w-full hover:scale-105"
> >
<div class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 border-2 border-dashed border-zinc-700"> <div
class="grow p-4 bg-zinc-800/50 hover:bg-zinc-800 border-2 border-dashed border-zinc-700"
>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<PlusIcon class="h-5 w-5 text-zinc-400 group-hover:text-zinc-300 transition-all duration-300 group-hover:rotate-90" /> <PlusIcon
<h3 class="text-lg font-semibold text-zinc-400 group-hover:text-zinc-300"> class="h-5 w-5 text-zinc-400 group-hover:text-zinc-300 transition-all duration-300 group-hover:rotate-90"
/>
<h3
class="text-lg font-semibold text-zinc-400 group-hover:text-zinc-300"
>
Create Collection Create Collection
</h3> </h3>
</div> </div>
@@ -78,10 +86,9 @@ import {
import { type Collection, type Game, type GameVersion } from "@prisma/client"; import { type Collection, type Game, type GameVersion } from "@prisma/client";
import { PlusIcon } from "@heroicons/vue/20/solid"; import { PlusIcon } from "@heroicons/vue/20/solid";
const headers = useRequestHeaders(["cookie"]); const gamesData = await $dropFetch<(Game & { versions: GameVersion[] })[]>(
const { data: gamesData } = await useFetch< "/api/v1/store/recent"
(Game & { versions: GameVersion[] })[] );
>("/api/v1/store/recent", { headers });
const collections = await useCollections(); const collections = await useCollections();
const collectionCreateOpen = ref(false); const collectionCreateOpen = ref(false);
+153 -146
View File
@@ -1,156 +1,163 @@
<template> <template>
<div class="flex flex-col lg:flex-row grow"> <div class="flex flex-col lg:flex-row grow">
<TransitionRoot as="template" :show="sidebarOpen"> <TransitionRoot as="template" :show="sidebarOpen">
<Dialog class="relative z-50 lg:hidden" @close="sidebarOpen = false"> <Dialog class="relative z-50 lg:hidden" @close="sidebarOpen = false">
<TransitionChild
as="template"
enter="transition-opacity ease-linear duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="transition-opacity ease-linear duration-300"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-zinc-900/80" />
</TransitionChild>
<div class="fixed inset-0 flex">
<TransitionChild <TransitionChild
as="template" as="template"
enter="transition-opacity ease-linear duration-300" enter="transition ease-in-out duration-300 transform"
enter-from="opacity-0" enter-from="-translate-x-full"
enter-to="opacity-100" enter-to="translate-x-0"
leave="transition-opacity ease-linear duration-300" leave="transition ease-in-out duration-300 transform"
leave-from="opacity-100" leave-from="translate-x-0"
leave-to="opacity-0" leave-to="-translate-x-full"
> >
<div class="fixed inset-0 bg-zinc-900/80" /> <DialogPanel class="relative mr-16 flex w-full max-w-xs flex-1">
</TransitionChild> <TransitionChild
as="template"
<div class="fixed inset-0 flex"> enter="ease-in-out duration-300"
<TransitionChild enter-from="opacity-0"
as="template" enter-to="opacity-100"
enter="transition ease-in-out duration-300 transform" leave="ease-in-out duration-300"
enter-from="-translate-x-full" leave-from="opacity-100"
enter-to="translate-x-0" leave-to="opacity-0"
leave="transition ease-in-out duration-300 transform" >
leave-from="translate-x-0" <div
leave-to="-translate-x-full" class="absolute top-0 left-full flex w-16 justify-center pt-5"
>
<DialogPanel class="relative mr-16 flex w-full max-w-xs flex-1">
<TransitionChild
as="template"
enter="ease-in-out duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="ease-in-out duration-300"
leave-from="opacity-100"
leave-to="opacity-0"
> >
<div <button
class="absolute top-0 left-full flex w-16 justify-center pt-5" type="button"
class="-m-2.5 p-2.5"
@click="sidebarOpen = false"
> >
<button <span class="sr-only">Close sidebar</span>
type="button" <XMarkIcon class="size-6 text-white" aria-hidden="true" />
class="-m-2.5 p-2.5" </button>
@click="sidebarOpen = false"
>
<span class="sr-only">Close sidebar</span>
<XMarkIcon class="size-6 text-white" aria-hidden="true" />
</button>
</div>
</TransitionChild>
<div class="bg-zinc-900">
<NewsDirectory ref="newsDirectory" />
</div> </div>
</DialogPanel> </TransitionChild>
</TransitionChild> <div class="bg-zinc-900">
</div> <NewsArticleCreateButton />
</Dialog> <NewsDirectory :articles="news" />
</TransitionRoot> </div>
</DialogPanel>
<!-- Static sidebar for desktop --> </TransitionChild>
<div
class="hidden lg:block lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col lg:border-r-2 lg:border-zinc-800"
>
<NewsDirectory ref="newsDirectory" />
</div>
<div
class="block flex items-center gap-x-2 bg-zinc-950 px-2 py-1 shadow-xs sm:px-4 lg:hidden border-b border-zinc-700"
>
<button
type="button"
class="-m-2.5 p-2.5 text-zinc-400 lg:hidden"
@click="sidebarOpen = true"
>
<span class="sr-only">Open sidebar</span>
<Bars3Icon class="size-6" aria-hidden="true" />
</button>
<div
class="flex-1 text-sm/6 font-semibold uppercase font-display text-zinc-400"
>
News
</div> </div>
</div> </Dialog>
</TransitionRoot>
<div class="px-4 py-10 sm:px-6 lg:px-8 lg:py-6 grow">
<NuxtPage /> <!-- Static sidebar for desktop -->
<div
class="hidden lg:block lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col lg:border-r-2 lg:border-zinc-800"
>
<NewsArticleCreateButton />
<NewsDirectory />
</div>
<div
class="block flex items-center gap-x-2 bg-zinc-950 px-2 py-1 shadow-xs sm:px-4 lg:hidden border-b border-zinc-700"
>
<button
type="button"
class="-m-2.5 p-2.5 text-zinc-400 lg:hidden"
@click="sidebarOpen = true"
>
<span class="sr-only">Open sidebar</span>
<Bars3Icon class="size-6" aria-hidden="true" />
</button>
<div
class="flex-1 text-sm/6 font-semibold uppercase font-display text-zinc-400"
>
News
</div> </div>
</div> </div>
</template>
<div class="px-4 py-10 sm:px-6 lg:px-8 lg:py-6 grow">
<script setup lang="ts"> <NuxtPage />
import { ref } from "vue"; </div>
import { </div>
Dialog, </template>
DialogPanel,
TransitionChild, <script setup lang="ts">
TransitionRoot, import { ref } from "vue";
} from "@headlessui/vue"; import {
import { Dialog,
Bars3Icon, DialogPanel,
CalendarIcon, TransitionChild,
ChartPieIcon, TransitionRoot,
DocumentDuplicateIcon, } from "@headlessui/vue";
FolderIcon, import {
HomeIcon, Bars3Icon,
UsersIcon, CalendarIcon,
XMarkIcon, ChartPieIcon,
} from "@heroicons/vue/24/outline"; DocumentDuplicateIcon,
FolderIcon,
const router = useRouter(); HomeIcon,
const sidebarOpen = ref(false); UsersIcon,
XMarkIcon,
router.afterEach(() => { } from "@heroicons/vue/24/outline";
sidebarOpen.value = false;
}); const news = useNews();
useHead({ if (!news.value) {
title: "News", await fetchNews();
}); console.log('fetched news')
</script> }
<style scoped> const router = useRouter();
const sidebarOpen = ref(false);
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar { router.afterEach(() => {
display: none; sidebarOpen.value = false;
} });
/* Hide scrollbar for IE, Edge and Firefox */ useHead({
.no-scrollbar { title: "News",
-ms-overflow-style: none; /* IE and Edge */ });
scrollbar-width: none; /* Firefox */ </script>
}
<style scoped>
.hover-lift { /* Hide scrollbar for Chrome, Safari and Opera */
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); .no-scrollbar::-webkit-scrollbar {
} display: none;
}
.hover-lift:hover {
transform: translateY(-2px) scale(1.02); /* Hide scrollbar for IE, Edge and Firefox */
box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.2); .no-scrollbar {
} -ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
/* Springy list animations */ }
.list-enter-active {
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); .hover-lift {
} transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.list-leave-active {
transition: all 0.3s ease; .hover-lift:hover {
} transform: translateY(-2px) scale(1.02);
box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.2);
.list-move { }
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
} /* Springy list animations */
</style> .list-enter-active {
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.list-leave-active {
transition: all 0.3s ease;
}
.list-move {
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
</style>
+9 -9
View File
@@ -70,10 +70,7 @@
</div> </div>
<!-- Article content - markdown --> <!-- Article content - markdown -->
<div <div class="mx-auto prose prose-invert prose-lg" v-html="renderedContent" />
class="mx-auto prose prose-invert prose-lg"
v-html="renderedContent"
/>
</div> </div>
<DeleteNewsModal v-model="currentlyDeleting" /> <DeleteNewsModal v-model="currentlyDeleting" />
@@ -85,16 +82,19 @@ import { TrashIcon } from "@heroicons/vue/24/outline";
import { micromark } from "micromark"; import { micromark } from "micromark";
const route = useRoute(); const route = useRoute();
const { data: article } = await useNews().getById(route.params.id as string);
const currentlyDeleting = ref(); const currentlyDeleting = ref();
const user = useUser(); const user = useUser();
const news = useNews();
if (!article.value) { if (!news.value) {
news.value = await fetchNews();
}
const article = computed(() => news.value?.find((e) => e.id == route.params.id));
if (!article.value)
throw createError({ throw createError({
statusCode: 404, statusCode: 404,
message: "Article not found", statusMessage: "Article not found",
fatal: true,
}); });
}
// Render markdown content // Render markdown content
const renderedContent = computed(() => { const renderedContent = computed(() => {
+10 -9
View File
@@ -10,8 +10,6 @@
Stay up to date with the latest updates and announcements. Stay up to date with the latest updates and announcements.
</p> </p>
</div> </div>
<NewsArticleCreate @refresh="refreshAll" />
</div> </div>
</div> </div>
@@ -83,9 +81,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { DocumentIcon } from "@heroicons/vue/24/outline"; import { DocumentIcon } from "@heroicons/vue/24/outline";
import type { Article } from "@prisma/client";
import type { SerializeObject } from "nitropack/types";
const newsDirectory = ref(); const props = defineProps<{
const { data: articles, refresh: refreshArticles } = await useNews().getAll(); articles: SerializeObject<
Article & {
tags: Array<{ name: string; id: string }>;
author: { displayName: string };
}
>[];
}>();
const formatDate = (date: string) => { const formatDate = (date: string) => {
return new Date(date).toLocaleDateString("en-AU", { return new Date(date).toLocaleDateString("en-AU", {
@@ -98,11 +104,6 @@ const formatDate = (date: string) => {
useHead({ useHead({
title: "News", title: "News",
}); });
const refreshAll = async () => {
await refreshArticles();
await newsDirectory.value?.refresh();
};
</script> </script>
<style scoped> <style scoped>
+1 -3
View File
@@ -176,10 +176,8 @@ const gameId = route.params.id.toString();
const user = useUser(); const user = useUser();
const headers = useRequestHeaders(["cookie"]);
const game = await $dropFetch<Game & { versions: GameVersion[] }>( const game = await $dropFetch<Game & { versions: GameVersion[] }>(
`/api/v1/games/${gameId}`, `/api/v1/games/${gameId}`
{ headers }
); );
// Preview description (first 30 lines) // Preview description (first 30 lines)
+9 -9
View File
@@ -35,7 +35,9 @@
{{ game.mShortDescription }} {{ game.mShortDescription }}
</p> </p>
<div> <div>
<div class="mt-8 grid grid-cols-1 lg:grid-cols-2 gap-4 w-fit mx-auto"> <div
class="mt-8 grid grid-cols-1 lg:grid-cols-2 gap-4 w-fit mx-auto"
>
<NuxtLink <NuxtLink
:href="`/store/${game.id}`" :href="`/store/${game.id}`"
class="block w-full rounded-md border border-transparent bg-white px-8 py-3 text-base font-medium text-gray-900 hover:bg-gray-100 sm:w-auto duration-200 hover:scale-105" class="block w-full rounded-md border border-transparent bg-white px-8 py-3 text-base font-medium text-gray-900 hover:bg-gray-100 sm:w-auto duration-200 hover:scale-105"
@@ -95,14 +97,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
const headers = useRequestHeaders(["cookie"]); const recent = await $dropFetch("/api/v1/store/recent");
const recent = await $dropFetch("/api/v1/store/recent", { headers }); const updated = await $dropFetch("/api/v1/store/updated");
const updated = await $dropFetch("/api/v1/store/updated", { headers }); const released = await $dropFetch("/api/v1/store/released");
const released = await $dropFetch("/api/v1/store/released", {
headers, const developers = await $dropFetch("/api/v1/store/developers");
}); const publishers = await $dropFetch("/api/v1/store/publishers");
const developers = await $dropFetch("/api/v1/store/developers", { headers });
const publishers = await $dropFetch("/api/v1/store/publishers", { headers });
useHead({ useHead({
title: "Store", title: "Store",
+9
View File
@@ -31,6 +31,15 @@ class NewsManager {
}, },
}, },
}, },
include: {
author: {
select: {
id: true,
displayName: true,
},
},
tags: true,
},
}); });
} }