Use nuxtopenfetch

This commit is contained in:
2025-10-10 20:13:29 +02:00
parent 1bce60af7d
commit 48d355d657
19 changed files with 2039 additions and 1660 deletions

View File

@@ -1,19 +1,25 @@
# Build stage # use the official Bun image
FROM node:20-alpine AS builder # see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 AS build
WORKDIR /app WORKDIR /app
COPY ./website /app
RUN npm install
RUN npm run generate
# Serve stage COPY website/package.json bun.lock* ./
FROM nginx:alpine3.17-slim
# Copy built files from Vite's dist folder # use ignore-scripts to avoid builting node modules like better-sqlite3
COPY --from=builder /app/.output/public /usr/share/nginx/html RUN bun install --frozen-lockfile --ignore-scripts
#COPY --from=builder /app/tranga-website/media /usr/share/nginx/html/media
COPY ./nginx /etc/nginx
EXPOSE 80 # Copy the entire project
ENV API_URL=http://tranga-api:6531 COPY website/* .
CMD ["nginx", "-g", "daemon off;"]
RUN bun --bun run build
# copy production dependencies and source code into final image
FROM oven/bun:1 AS production
WORKDIR /app
# Only `.output` folder is needed from the build stage
COPY --from=build /app/.output /app
# run the app
EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "--bun", "run", "/app/server/index.mjs" ]

View File

@@ -6,14 +6,14 @@
<NuxtLink to="https://github.com/C9Glax/tranga-website" <NuxtLink to="https://github.com/C9Glax/tranga-website"
><Icon name="i-lucide-github" />Website</NuxtLink ><Icon name="i-lucide-github" />Website</NuxtLink
> >
<NuxtLink :to="`${$config.public.apiParty.endpoints.api?.url}swagger`" <NuxtLink :to="`${$config.public.openFetch.api.baseURL}swagger`"
><Icon name="i-lucide-book-open" />Swagger</NuxtLink ><Icon name="i-lucide-book-open" />Swagger</NuxtLink
> >
</template> </template>
<template #default> <template #default>
<NuxtLink to="/"> <NuxtLink to="/">
<div class="h-full flex gap-2 items-center"> <div class="h-full flex gap-2 items-center">
<img src="/blahaj.png" class="h-lh cursor-grab" /> <img src="/blahaj.png" class="h-lh cursor-grab" >
<p <p
style=" style="
background: linear-gradient(110deg, var(--color-pink), var(--color-blue)); background: linear-gradient(110deg, var(--color-pink), var(--color-blue));

View File

@@ -15,20 +15,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ApiModel } from '#nuxt-api-party'; import type { components } from '#open-fetch-schemas/api';
type CreateLibraryRecord = components['schemas']['CreateLibraryRecord'];
const name = ref(''); const name = ref('');
const path = ref(''); const path = ref('');
const model = computed((): ApiModel<'CreateLibraryRecord'> => { const model: ComputedRef = computed((): CreateLibraryRecord => {
return { basePath: path.value, libraryName: name.value }; return { basePath: path.value, libraryName: name.value };
}); });
const busy = ref(false); const busy = ref(false);
const onAddClick = async () => { const onAddClick = async () => {
if (!model.value) return;
busy.value = true; busy.value = true;
await $api('/v2/FileLibrary', { method: 'PUT', body: model.value }) await useApi('/v2/FileLibrary', { method: 'PUT', body: model.value })
.then(() => refreshNuxtData(Keys.FileLibraries)) .then(() => refreshNuxtData(FetchKeys.FileLibraries))
.finally(() => (busy.value = false)); .finally(() => (busy.value = false));
}; };
</script> </script>

View File

@@ -36,7 +36,7 @@ export interface ChaptersListProps {
} }
const props = defineProps<ChaptersListProps>(); const props = defineProps<ChaptersListProps>();
const { data: chapters } = await useApiData('/v2/Manga/{MangaId}/Chapters', { const { data: chapters } = await useApi('/v2/Manga/{MangaId}/Chapters', {
path: { MangaId: props.mangaId }, path: { MangaId: props.mangaId },
key: FetchKeys.Chapters.All, key: FetchKeys.Chapters.All,
}); });

View File

@@ -13,14 +13,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ApiModel } from '#nuxt-api-party'; import type { components } from '#open-fetch-schemas/api';
type FileLibrary = ApiModel<'FileLibrary'>; type FileLibrary = components['schemas']['FileLibrary'];
const { data: fileLibraries } = await useApiData('/v2/FileLibrary', { key: FetchKeys.FileLibraries }); const { data: fileLibraries } = await useApi('/v2/FileLibrary', { key: FetchKeys.FileLibraries });
const busy = ref(false); const busy = ref(false);
const deleteLibrary = async (l: FileLibrary) => { const deleteLibrary = async (library: FileLibrary) => {
busy.value = true; busy.value = true;
await $api('/v2/FileLibrary/{FileLibraryId}', { path: { FileLibraryId: l.key }, method: 'DELETE' }) await useApi('/v2/FileLibrary/{FileLibraryId}', { path: { FileLibraryId: library.key }, method: 'DELETE' })
.then(() => refreshNuxtData(FetchKeys.FileLibraries)) .then(() => refreshNuxtData(FetchKeys.FileLibraries))
.finally(() => (busy.value = false)); .finally(() => (busy.value = false));
}; };

View File

@@ -33,10 +33,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ApiModel } from '#nuxt-api-party'; import type { components } from '#open-fetch-schemas/api';
import type { PageCardProps } from '#ui/components/PageCard.vue'; import type { PageCardProps } from '#ui/components/PageCard.vue';
type Manga = ApiModel<'Manga'>; type Manga = components['schemas']['Manga'];
type MinimalManga = ApiModel<'MinimalManga'>; type MinimalManga = components['schemas']['MinimalManga'];
defineProps<MangaCardProps>(); defineProps<MangaCardProps>();
defineEmits(['click']); defineEmits(['click']);

View File

@@ -12,14 +12,15 @@
<p class="p-3 text-xl font-semibold max-h-full overflow-clip">{{ manga?.name }}</p> <p class="p-3 text-xl font-semibold max-h-full overflow-clip">{{ manga?.name }}</p>
</div> </div>
<LazyNuxtImg <LazyNuxtImg
:src="`${$config.public.apiParty.endpoints.Api!.url}v2/Manga/${manga.key}/Cover/Medium`" :src="`${$config.public.openFetch.api.baseURL}v2/Manga/${manga.key}/Cover/Medium`"
class="w-full h-full object-cover" /> class="w-full h-full object-cover" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ApiModel } from '#nuxt-api-party'; import type { components } from '#open-fetch-schemas/api';
type MinimalManga = ApiModel<'MinimalManga'>; type Manga = components['schemas']['Manga'];
type Manga = ApiModel<'Manga'>; type MinimalManga = components['schemas']['MinimalManga'];
defineProps<{ manga: Manga | MinimalManga; blur?: boolean }>(); defineProps<{ manga: Manga | MinimalManga; blur?: boolean }>();
</script> </script>

View File

@@ -40,8 +40,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ApiModel } from '#nuxt-api-party'; import type { components } from '#open-fetch-schemas/api';
type Manga = ApiModel<'Manga'>; type Manga = components['schemas']['Manga'];
export interface MangaDetailPageProps { export interface MangaDetailPageProps {
manga?: Manga; manga?: Manga;

View File

@@ -14,12 +14,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ApiModel } from '#nuxt-api-party'; import type { components } from '#open-fetch-schemas/api';
type MangaConnectorId = /* @vue-ignore */ ApiModel<'MangaConnectorId'>; type MangaConnectorId = components['schemas']['MangaConnectorId'];
const props = defineProps<MangaConnectorId>(); const props = defineProps<MangaConnectorId>();
const { data: mangaConnector } = await useApiData('/v2/MangaConnector/{MangaConnectorName}', { const { data: mangaConnector } = await useApi('/v2/MangaConnector/{MangaConnectorName}', {
path: { MangaConnectorName: props.mangaConnectorName }, path: { MangaConnectorName: props.mangaConnectorName },
key: FetchKeys.MangaConnector.Id(props.mangaConnectorName), key: FetchKeys.MangaConnector.Id(props.mangaConnectorName),
}); });

View File

@@ -8,7 +8,7 @@ import MangaDetailPage from '~/components/MangaDetailPage.vue';
const route = useRoute(); const route = useRoute();
const mangaId = route.params.MangaId as string; const mangaId = route.params.MangaId as string;
const { data: manga } = await useApiData('/v2/Manga/{MangaId}', { const { data: manga } = await useApi('/v2/Manga/{MangaId}', {
path: { MangaId: mangaId }, path: { MangaId: mangaId },
key: FetchKeys.Manga.Id(mangaId), key: FetchKeys.Manga.Id(mangaId),
}); });

View File

@@ -13,8 +13,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { data: manga } = await useApiData('/v2/Manga', { key: FetchKeys.Manga.All }); const { data: manga } = await useApi('/v2/Manga', { key: FetchKeys.Manga.All });
const expanded = ref(-1); const expanded = ref(-1);
</script> </script>
<style scoped></style>

View File

@@ -11,9 +11,9 @@
import MangaDetailPage from '~/components/MangaDetailPage.vue'; import MangaDetailPage from '~/components/MangaDetailPage.vue';
const route = useRoute(); const route = useRoute();
const mangaId = route.params.MangaId as string; const mangaId = route.params.mangaId as string;
const { data: manga } = await useApiData('/v2/Manga/{MangaId}', { const { data: manga } = await useApi('/v2/Manga/{MangaId}', {
path: { MangaId: mangaId }, path: { MangaId: mangaId },
key: FetchKeys.Manga.Id(mangaId), key: FetchKeys.Manga.Id(mangaId),
}); });

View File

@@ -24,11 +24,11 @@ const route = useRoute();
const targetId = route.params.targetId as string; const targetId = route.params.targetId as string;
const mangaId = route.params.mangaId as string; const mangaId = route.params.mangaId as string;
const { data: target } = await useApiData('/v2/Manga/{MangaId}', { const { data: target } = await useApi('/v2/Manga/{MangaId}', {
path: { MangaId: targetId }, path: { MangaId: targetId },
key: FetchKeys.Manga.Id(targetId), key: FetchKeys.Manga.Id(targetId),
}); });
const { data: manga } = await useApiData('/v2/Manga/{MangaId}', { const { data: manga } = await useApi('/v2/Manga/{MangaId}', {
path: { MangaId: mangaId }, path: { MangaId: mangaId },
key: FetchKeys.Manga.Id(mangaId), key: FetchKeys.Manga.Id(mangaId),
}); });

View File

@@ -12,10 +12,11 @@
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute(); const route = useRoute();
const mangaId = route.params.mangaId as string;
const { data: manga } = await useApiData('/v2/Manga/{MangaId}', { const { data: manga } = await useApi('/v2/Manga/{MangaId}', {
path: { MangaId: route.params.mangaId as string }, path: { MangaId: mangaId },
key: FetchKeys.Manga.Id(mangaId), key: FetchKeys.Manga.Id(mangaId),
}); });
const { data: mangas } = await useApiData('/v2/Manga', { key: FetchKeys.Manga.All }); const { data: mangas } = await useApi('/v2/Manga', { key: FetchKeys.Manga.All });
</script> </script>

View File

@@ -46,12 +46,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { $api, type ApiModel } from '#nuxt-api-party'; import type { components } from '#open-fetch-schemas/api';
import type { StepperItem } from '@nuxt/ui'; import type { StepperItem } from '@nuxt/ui';
type MangaConnector = ApiModel<'MangaConnector'>; import type { AsyncData, FetchResult } from '#app';
type MinimalManga = ApiModel<'MinimalManga'>; type MangaConnector = components['schemas']['MangaConnector'];
type MinimalManga = components['schemas']['MinimalManga'];
const { data: connectors } = await useApiData('/v2/MangaConnector', { FetchKeys: FetchKeys.MangaConnector.All }); const { data: connectors } = await useApi('/v2/MangaConnector', { key: FetchKeys.MangaConnector.All });
const query = ref<string>(); const query = ref<string>();
const connector = useState<MangaConnector>(); const connector = useState<MangaConnector>();
@@ -91,21 +92,19 @@ const performSearch = () => {
.finally(() => (busy.value = false)); .finally(() => (busy.value = false));
}; };
const config = useRuntimeConfig();
const search = async (query: string): Promise<MinimalManga[]> => { const search = async (query: string): Promise<MinimalManga[]> => {
if (isUrl(query)) { if (isUrl(query)) {
return await $api<'/v2/Search/Url', MinimalManga>('/v2/Search/Url', { const { data } = await useApi('/v2/Search/Url', { body: JSON.stringify(query), method: 'POST' });
body: JSON.stringify(query), if (data.value) return [data.value];
method: 'POST', else return Promise.reject();
}).then((x) => [x]); } else if (connector.value.name) {
} else if (connector.value) { const { data } = await useApi('/v2/Search/{MangaConnectorName}/{Query}', {
return await $api('/v2/Search/{MangaConnectorName}/{Query}', { path: { MangaConnectorName: connector.value.name, Query: query },
path: { MangaConnectorName: connector.value.name, query: query }, method: 'GET',
method: 'POST',
}); });
} if (data.value) return data.value;
return Promise.reject(); else return Promise.reject();
} else return Promise.reject();
}; };
const items = ref<StepperItem[]>([ const items = ref<StepperItem[]>([

View File

@@ -2,7 +2,7 @@
<UPageSection title="Settings" /> <UPageSection title="Settings" />
<UPageSection title="Libraries" orientation="horizontal"> <UPageSection title="Libraries" orientation="horizontal">
<template #footer> <template #footer>
<UButton icon="i-lucide-plus" class="w-fit" @click="() => addLibraryModal.open()">Add</UButton> <UButton icon="i-lucide-plus" class="w-fit" @click="addLibraryModal.open()">Add</UButton>
</template> </template>
<FileLibraries /> <FileLibraries />
</UPageSection> </UPageSection>
@@ -17,19 +17,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { LazyAddLibraryModal } from '#components'; import { LazyAddLibraryModal } from '#components';
import FileLibraries from '~/components/FileLibraries.vue'; import FileLibraries from '~/components/FileLibraries.vue';
import { refreshNuxtData } from '#app'; import { refreshNuxtData } from '#app';
const overlay = useOverlay(); const overlay = useOverlay();
const config = useRuntimeConfig();
const addLibraryModal = overlay.create(LazyAddLibraryModal); const addLibraryModal = overlay.create(LazyAddLibraryModal);
const cleanUpDatabaseBusy = ref(false); const cleanUpDatabaseBusy = ref(false);
const cleanUpDatabase = async () => { const cleanUpDatabase = async () => {
cleanUpDatabaseBusy.value = true; cleanUpDatabaseBusy.value = true;
await $api('/v2/Maintenance/CleanupNoDownloadManga', { method: 'POST' }) await useApi('/v2/Maintenance/CleanupNoDownloadManga', { method: 'POST' })
.then(() => refreshNuxtData(Keys.Manga.All)) .then(() => refreshNuxtData(FetchKeys.Manga.All))
.finally(() => (cleanUpDatabaseBusy.value = false)); .finally(() => (cleanUpDatabaseBusy.value = false));
}; };
</script> </script>

View File

@@ -3,18 +3,16 @@ import tailwindcss from '@tailwindcss/vite';
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: '2025-07-15', compatibilityDate: '2025-07-15',
devtools: { enabled: true }, devtools: { enabled: true },
vite: { plugins: [tailwindcss()] },
css: ['~/assets/css/main.css'], css: ['~/assets/css/main.css'],
modules: ['@nuxt/content', '@nuxt/eslint', '@nuxt/image', '@nuxt/ui', 'nuxt-api-party'], modules: ['@nuxt/content', '@nuxt/eslint', '@nuxt/image', '@nuxt/ui', 'nuxt-open-fetch'],
devServer: { host: '127.0.0.1' }, devServer: { host: '127.0.0.1' },
runtimeConfig: { openFetch: {
apiParty: { clients: {
endpoints: {
api: { api: {
url: 'http://127.0.0.1:6531', baseURL: 'http://127.0.0.1:6531/',
schema: 'https://raw.githubusercontent.com/C9Glax/tranga/refs/heads/testing/API/openapi/API_v2.json', schema: 'https://raw.githubusercontent.com/C9Glax/tranga/refs/heads/testing/API/openapi/API_v2.json',
}, },
}, },
}, },
}, vite: { plugins: [tailwindcss()] },
}); });

3527
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,18 +17,17 @@
"@nuxt/eslint": "^1.9.0", "@nuxt/eslint": "^1.9.0",
"@nuxt/image": "^1.11.0", "@nuxt/image": "^1.11.0",
"@nuxt/ui": "^4.0.0", "@nuxt/ui": "^4.0.0",
"@tailwindcss/vite": "^4.1.13", "@tailwindcss/vite": "^4.1.14",
"better-sqlite3": "^12.4.1", "better-sqlite3": "^12.4.1",
"nuxt": "^4.1.2", "nuxt": "^4.1.2",
"nuxt-api-party": "^3.3.0", "tailwindcss": "^4.1.14"
"tailwindcss": "^4.1.13",
"vue": "^3.5.21",
"vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/lucide": "^1.2.68", "@iconify-json/lucide": "^1.2.68",
"eslint": "^9.36.0", "eslint": "^9.36.0",
"nuxt-open-fetch": "^0.13.5",
"openapi-typescript": "^7.9.1", "openapi-typescript": "^7.9.1",
"postcss": "^8.4.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"typescript": "^5.9.2" "typescript": "^5.9.2"
} }