mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-10-11 13:19:49 +02:00
Use nuxtopenfetch
This commit is contained in:
36
Dockerfile
36
Dockerfile
@@ -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" ]
|
||||||
|
@@ -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));
|
||||||
|
@@ -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>
|
||||||
|
@@ -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,
|
||||||
});
|
});
|
||||||
|
@@ -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));
|
||||||
};
|
};
|
||||||
|
@@ -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']);
|
||||||
|
@@ -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>
|
||||||
|
@@ -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;
|
||||||
|
@@ -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),
|
||||||
});
|
});
|
||||||
|
@@ -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),
|
||||||
});
|
});
|
||||||
|
@@ -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>
|
|
||||||
|
@@ -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),
|
||||||
});
|
});
|
||||||
|
@@ -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),
|
||||||
});
|
});
|
||||||
|
@@ -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>
|
||||||
|
@@ -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[]>([
|
||||||
|
@@ -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>
|
||||||
|
@@ -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: {
|
baseURL: 'http://127.0.0.1:6531/',
|
||||||
url: '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
3527
website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user