mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-07-24 08:53:04 +10:00
feat: refactor news and migrate rest of useFetch to $dropFetch
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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.";
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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<{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
+139
-132
@@ -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">
|
<!-- Static sidebar for desktop -->
|
||||||
<NuxtPage />
|
<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>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<div class="px-4 py-10 sm:px-6 lg:px-8 lg:py-6 grow">
|
||||||
import { ref } from "vue";
|
<NuxtPage />
|
||||||
import {
|
</div>
|
||||||
Dialog,
|
</div>
|
||||||
DialogPanel,
|
</template>
|
||||||
TransitionChild,
|
|
||||||
TransitionRoot,
|
|
||||||
} from "@headlessui/vue";
|
|
||||||
import {
|
|
||||||
Bars3Icon,
|
|
||||||
CalendarIcon,
|
|
||||||
ChartPieIcon,
|
|
||||||
DocumentDuplicateIcon,
|
|
||||||
FolderIcon,
|
|
||||||
HomeIcon,
|
|
||||||
UsersIcon,
|
|
||||||
XMarkIcon,
|
|
||||||
} from "@heroicons/vue/24/outline";
|
|
||||||
|
|
||||||
const router = useRouter();
|
<script setup lang="ts">
|
||||||
const sidebarOpen = ref(false);
|
import { ref } from "vue";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
TransitionChild,
|
||||||
|
TransitionRoot,
|
||||||
|
} from "@headlessui/vue";
|
||||||
|
import {
|
||||||
|
Bars3Icon,
|
||||||
|
CalendarIcon,
|
||||||
|
ChartPieIcon,
|
||||||
|
DocumentDuplicateIcon,
|
||||||
|
FolderIcon,
|
||||||
|
HomeIcon,
|
||||||
|
UsersIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
} from "@heroicons/vue/24/outline";
|
||||||
|
|
||||||
router.afterEach(() => {
|
const news = useNews();
|
||||||
sidebarOpen.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
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 */
|
router.afterEach(() => {
|
||||||
.no-scrollbar::-webkit-scrollbar {
|
sidebarOpen.value = false;
|
||||||
display: none;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide scrollbar for IE, Edge and Firefox */
|
useHead({
|
||||||
.no-scrollbar {
|
title: "News",
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
});
|
||||||
scrollbar-width: none; /* Firefox */
|
</script>
|
||||||
}
|
|
||||||
|
|
||||||
.hover-lift {
|
<style scoped>
|
||||||
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
}
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.hover-lift:hover {
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
transform: translateY(-2px) scale(1.02);
|
.no-scrollbar {
|
||||||
box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.2);
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
}
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
/* Springy list animations */
|
.hover-lift {
|
||||||
.list-enter-active {
|
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.list-leave-active {
|
.hover-lift:hover {
|
||||||
transition: all 0.3s ease;
|
transform: translateY(-2px) scale(1.02);
|
||||||
}
|
box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.list-move {
|
/* Springy list animations */
|
||||||
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
.list-enter-active {
|
||||||
}
|
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
</style>
|
}
|
||||||
|
|
||||||
|
.list-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-move {
|
||||||
|
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ class NewsManager {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
author: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
displayName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user