Compare commits

...

13 Commits

Author SHA1 Message Date
Rodrigo Verdiani
39710d9135 feature: ci/cd pipeline
# Conflicts:
#	api/mangamochi.ts
2025-10-28 22:28:23 -03:00
Rodrigo Verdiani
8196548f1d woodpecker test 9 2025-10-28 22:27:33 -03:00
Rodrigo Verdiani
9b5572c970 woodpecker test 8 2025-10-28 22:27:33 -03:00
Rodrigo Verdiani
b5fff43393 woodpecker test 7 2025-10-28 22:27:33 -03:00
Rodrigo Verdiani
b5bb6c7e87 woodpecker test 6 2025-10-28 22:27:33 -03:00
Rodrigo Verdiani
7475e8abae woodpecker test 5 2025-10-28 22:27:33 -03:00
Rodrigo Verdiani
c14ef1bc56 woodpecker test 4 2025-10-28 22:27:33 -03:00
Rodrigo Verdiani
ab8640a831 woodpecker test 3 2025-10-28 22:27:33 -03:00
Rodrigo Verdiani
fa52533b1a woodpecker test 2 2025-10-28 22:27:33 -03:00
Rodrigo Verdiani
21f6fc1a0e woodpecker test 1 2025-10-28 22:27:33 -03:00
Rodrigo Verdiani
bf51ae4656 woodpecker test 2025-10-28 22:27:33 -03:00
Rodrigo Verdiani
f872d96b80 feat: add functionality to fetch all chapters from provider and update related hooks 2025-10-27 17:10:54 -03:00
rov
6c8ed19be4 Merge pull request 'feature/pipeline: add pipeline to project' (#2) from feature/pipeline into main
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
Reviewed-on: #2
2025-10-26 21:42:02 -03:00
8 changed files with 230 additions and 57 deletions

4
.gitignore vendored
View File

@ -142,7 +142,7 @@ yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
.env.development
# vercel
.vercel
@ -226,7 +226,7 @@ web_modules/
.env.development.local
.env.test.local
.env.production.local
.env.local
.env.development
# parcel-bundler cache (https://parceljs.org/)
.cache

View File

@ -1,30 +1,57 @@
# .woodpecker.yml
# .pipeline.yml
# -----------------
# Run pipeline on pushes to main and pull requests
when:
event: [push, pull_request]
branch: [main, develop, feat/*, feature/*]
event: [ push, pull_request ]
# Define environment variables
environment:
NODE_ENV: production
# Pipeline steps
steps:
# Install dependencies
- name: install
image: node:18-alpine
commands:
- echo "🔧 Installing dependencies..."
- npm ci --silent
- name: publish-image
image: woodpeckerci/plugin-docker-buildx
settings:
platforms: linux/amd64
repo: git.badger-pirarucu.ts.net/mangamochi/frontend
registry: git.badger-pirarucu.ts.net
dockerfile: Dockerfile
context: .
username:
from_secret: DOCKER_USER
password:
from_secret: DOCKER_PASSWORD
tags:
- latest
- ${CI_COMMIT_SHA}
when:
event: [push, pull_request]
event: [ push, pull_request ]
branch: [ main, dev, feature/*, feat/* ]
# Build application
- name: build
image: node:18-alpine
- name: deploy
depends_on: [ publish-image ]
image: alpine:3.20
environment:
DEPLOY_USER: rov
DEPLOY_HOST: mangamochi.badger-pirarucu.ts.net
DEPLOY_PORT: 22
IMAGE: git.badger-pirarucu.ts.net/mangamochi/frontend:${CI_COMMIT_SHA}
DEPLOY_SSH_KEY:
from_secret: DEPLOY_SSH_KEY
commands:
- echo "🏗️ Building application..."
- npm run build
- echo "🚀 Deploying Next.js app to $DEPLOY_HOST..."
- apk add --no-cache openssh-client docker-cli
- mkdir -p ~/.ssh
- echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -p $DEPLOY_PORT $DEPLOY_HOST >> ~/.ssh/known_hosts
- >
ssh -p $DEPLOY_PORT $DEPLOY_USER@$DEPLOY_HOST "
docker pull $IMAGE &&
docker stop mangamochi-frontend 2>/dev/null || true &&
docker rm mangamochi-frontend 2>/dev/null || true &&
docker run -d --name mangamochi-frontend \
--restart always \
--env-file /home/rov/mangamochi/.env \
-p 80:3000 \
$IMAGE
"
when:
event: [push, pull_request]
event: [ push, pull_request ]
branch: [ main, dev, feature/*, feat/* ]

View File

@ -3,7 +3,7 @@ import { User } from "@/contexts/auth-context";
import { toast } from "sonner";
export const Api = axios.create({
baseURL: "http://localhost:8080",
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
responseType: "json",
});

View File

@ -24,6 +24,10 @@ import type {
} from '@tanstack/react-query';
import { customInstance } from './api';
export interface UpdateMangaDataCommand {
mangaId?: number;
}
export interface DefaultResponseDTOVoid {
timestamp?: string;
data?: unknown;
@ -268,6 +272,65 @@ type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
export const sendRecord = (
updateMangaDataCommand: UpdateMangaDataCommand,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<string>(
{url: `/records`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: updateMangaDataCommand, signal
},
options);
}
export const getSendRecordMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof sendRecord>>, TError,{data: UpdateMangaDataCommand}, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof sendRecord>>, TError,{data: UpdateMangaDataCommand}, TContext> => {
const mutationKey = ['sendRecord'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof sendRecord>>, {data: UpdateMangaDataCommand}> = (props) => {
const {data} = props ?? {};
return sendRecord(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type SendRecordMutationResult = NonNullable<Awaited<ReturnType<typeof sendRecord>>>
export type SendRecordMutationBody = UpdateMangaDataCommand
export type SendRecordMutationError = unknown
export const useSendRecord = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof sendRecord>>, TError,{data: UpdateMangaDataCommand}, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof sendRecord>>,
TError,
{data: UpdateMangaDataCommand},
TContext
> => {
const mutationOptions = getSendRecordMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* Fetch a list of manga chapters for a specific manga/provider combination.
* @summary Fetch the available chapters for a specific manga/provider combination
@ -279,7 +342,7 @@ export const fetchMangaChapters = (
return customInstance<DefaultResponseDTOVoid>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(mangaProviderId))}/fetch-chapters`, method: 'POST', signal
{url: `/mangas/${encodeURIComponent(String(mangaProviderId))}/fetch-chapters`, method: 'POST', signal
},
options);
}
@ -332,6 +395,70 @@ export const useFetchMangaChapters = <TError = unknown,
return useMutation(mutationOptions, queryClient);
}
/**
* Fetch all not yet downloaded chapters from the provider
* @summary Fetch all chapters
*/
export const fetchAllChapters = (
mangaProviderId: number,
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOVoid>(
{url: `/mangas/${encodeURIComponent(String(mangaProviderId))}/fetch-all-chapters`, method: 'POST', signal
},
options);
}
export const getFetchAllChaptersMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof fetchAllChapters>>, TError,{mangaProviderId: number}, TContext>, request?: SecondParameter<typeof customInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof fetchAllChapters>>, TError,{mangaProviderId: number}, TContext> => {
const mutationKey = ['fetchAllChapters'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof fetchAllChapters>>, {mangaProviderId: number}> = (props) => {
const {mangaProviderId} = props ?? {};
return fetchAllChapters(mangaProviderId,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type FetchAllChaptersMutationResult = NonNullable<Awaited<ReturnType<typeof fetchAllChapters>>>
export type FetchAllChaptersMutationError = unknown
/**
* @summary Fetch all chapters
*/
export const useFetchAllChapters = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof fetchAllChapters>>, TError,{mangaProviderId: number}, TContext>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof fetchAllChapters>>,
TError,
{mangaProviderId: number},
TContext
> => {
const mutationOptions = getFetchAllChaptersMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* Remove a manga from favorites for the logged user.
* @summary Unfavorite a manga
@ -343,7 +470,7 @@ export const setUnfavorite = (
return customInstance<DefaultResponseDTOVoid>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(id))}/unfavorite`, method: 'POST', signal
{url: `/mangas/${encodeURIComponent(String(id))}/unfavorite`, method: 'POST', signal
},
options);
}
@ -407,7 +534,7 @@ export const setFavorite = (
return customInstance<DefaultResponseDTOVoid>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(id))}/favorite`, method: 'POST', signal
{url: `/mangas/${encodeURIComponent(String(id))}/favorite`, method: 'POST', signal
},
options);
}
@ -471,7 +598,7 @@ export const markAsRead = (
return customInstance<DefaultResponseDTOVoid>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/mangas/chapters/${encodeURIComponent(String(chapterId))}/mark-as-read`, method: 'POST', signal
{url: `/mangas/chapters/${encodeURIComponent(String(chapterId))}/mark-as-read`, method: 'POST', signal
},
options);
}
@ -535,7 +662,7 @@ export const fetchChapter = (
return customInstance<DefaultResponseDTOVoid>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/mangas/chapters/${encodeURIComponent(String(chapterId))}/fetch`, method: 'POST', signal
{url: `/mangas/chapters/${encodeURIComponent(String(chapterId))}/fetch`, method: 'POST', signal
},
options);
}
@ -600,7 +727,7 @@ export const downloadChapterArchive = (
return customInstance<Blob>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/mangas/chapters/${encodeURIComponent(String(chapterId))}/download`, method: 'POST',
{url: `/mangas/chapters/${encodeURIComponent(String(chapterId))}/download`, method: 'POST',
params,
responseType: 'blob', signal
},
@ -669,7 +796,7 @@ formData.append(`malId`, importMultipleFilesBody.malId)
importMultipleFilesBody.files.forEach(value => formData.append(`files`, value));
return customInstance<DefaultResponseDTOVoid>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/manga/import/upload`, method: 'POST',
{url: `/manga/import/upload`, method: 'POST',
headers: {'Content-Type': 'multipart/form-data', },
data: formData, signal
},
@ -735,7 +862,7 @@ export const getImportReviews = (
return customInstance<DefaultResponseDTOListImportReviewDTO>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/manga/import/review`, method: 'GET', signal
{url: `/manga/import/review`, method: 'GET', signal
},
options);
}
@ -745,7 +872,7 @@ export const getImportReviews = (
export const getGetImportReviewsQueryKey = () => {
return [
`http://mangamochi.badger-pirarucu.ts.net:8080/manga/import/review`
`/manga/import/review`
] as const;
}
@ -828,7 +955,7 @@ export const resolveImportReview = (
return customInstance<DefaultResponseDTOVoid>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/manga/import/review`, method: 'POST',
{url: `/manga/import/review`, method: 'POST',
params, signal
},
options);
@ -893,7 +1020,7 @@ export const importFromMangaDex = (
return customInstance<DefaultResponseDTOImportMangaDexResponseDTO>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/manga/import/manga-dex`, method: 'POST',
{url: `/manga/import/manga-dex`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: importMangaDexRequestDTO, signal
},
@ -959,7 +1086,7 @@ export const registerUser = (
return customInstance<DefaultResponseDTOVoid>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/auth/register`, method: 'POST',
{url: `/auth/register`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: registrationRequestDTO, signal
},
@ -1025,7 +1152,7 @@ export const authenticateUser = (
return customInstance<DefaultResponseDTOAuthenticationResponseDTO>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/auth/login`, method: 'POST',
{url: `/auth/login`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: authenticationRequestDTO, signal
},
@ -1091,7 +1218,7 @@ export const getMangas = (
return customInstance<DefaultResponseDTOPageMangaListDTO>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/mangas`, method: 'GET',
{url: `/mangas`, method: 'GET',
params, signal
},
options);
@ -1102,7 +1229,7 @@ export const getMangas = (
export const getGetMangasQueryKey = (params?: GetMangasParams,) => {
return [
`http://mangamochi.badger-pirarucu.ts.net:8080/mangas`, ...(params ? [params]: [])
`/mangas`, ...(params ? [params]: [])
] as const;
}
@ -1185,7 +1312,7 @@ export const getMangaChapters = (
return customInstance<DefaultResponseDTOListMangaChapterDTO>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(mangaProviderId))}/chapters`, method: 'GET', signal
{url: `/mangas/${encodeURIComponent(String(mangaProviderId))}/chapters`, method: 'GET', signal
},
options);
}
@ -1195,7 +1322,7 @@ export const getMangaChapters = (
export const getGetMangaChaptersQueryKey = (mangaProviderId?: number,) => {
return [
`http://mangamochi.badger-pirarucu.ts.net:8080/mangas/${mangaProviderId}/chapters`
`/mangas/${mangaProviderId}/chapters`
] as const;
}
@ -1278,7 +1405,7 @@ export const getManga = (
return customInstance<DefaultResponseDTOMangaDTO>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(mangaId))}`, method: 'GET', signal
{url: `/mangas/${encodeURIComponent(String(mangaId))}`, method: 'GET', signal
},
options);
}
@ -1288,7 +1415,7 @@ export const getManga = (
export const getGetMangaQueryKey = (mangaId?: number,) => {
return [
`http://mangamochi.badger-pirarucu.ts.net:8080/mangas/${mangaId}`
`/mangas/${mangaId}`
] as const;
}
@ -1371,7 +1498,7 @@ export const getMangaChapterImages = (
return customInstance<DefaultResponseDTOMangaChapterImagesDTO>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/mangas/chapters/${encodeURIComponent(String(chapterId))}/images`, method: 'GET', signal
{url: `/mangas/chapters/${encodeURIComponent(String(chapterId))}/images`, method: 'GET', signal
},
options);
}
@ -1381,7 +1508,7 @@ export const getMangaChapterImages = (
export const getGetMangaChapterImagesQueryKey = (chapterId?: number,) => {
return [
`http://mangamochi.badger-pirarucu.ts.net:8080/mangas/chapters/${chapterId}/images`
`/mangas/chapters/${chapterId}/images`
] as const;
}
@ -1464,7 +1591,7 @@ export const getGenres = (
return customInstance<DefaultResponseDTOListGenreDTO>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/genres`, method: 'GET', signal
{url: `/genres`, method: 'GET', signal
},
options);
}
@ -1474,7 +1601,7 @@ export const getGenres = (
export const getGetGenresQueryKey = () => {
return [
`http://mangamochi.badger-pirarucu.ts.net:8080/genres`
`/genres`
] as const;
}
@ -1556,7 +1683,7 @@ export const deleteImportReview = (
return customInstance<DefaultResponseDTOVoid>(
{url: `http://mangamochi.badger-pirarucu.ts.net:8080/manga/import/review/${encodeURIComponent(String(id))}`, method: 'DELETE'
{url: `/manga/import/review/${encodeURIComponent(String(id))}`, method: 'DELETE'
},
options);
}

View File

@ -142,8 +142,7 @@ export default function ChapterReaderPage() {
{/* Manga Page */}
<div className="relative mx-auto mb-8 overflow-hidden rounded-lg border border-border bg-muted">
<Image
src={
"http://omv.badger-pirarucu.ts.net:9000/mangamochi/" +
src={process.env.NEXT_PUBLIC_OMV_BASE_URL + "/" +
data.data.chapterImageKeys[currentPage - 1] ||
"/placeholder.svg"
}

View File

@ -20,9 +20,10 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ThemeToggle } from "@/components/theme-toggle";
import { useFetchMangaChapters, useGetManga } from "@/api/mangamochi";
import {useFetchAllChapters, useFetchMangaChapters, useGetManga} from "@/api/mangamochi";
import { MangaChapter } from "@/components/manga-chapter";
import { useQueryClient } from "@tanstack/react-query";
import {toast} from "sonner";
export default function MangaDetailPage() {
const params = useParams();
@ -33,12 +34,20 @@ export default function MangaDetailPage() {
const { data: mangaData, queryKey } = useGetManga(mangaId);
const { mutate, isPending } = useFetchMangaChapters({
const { mutate, isPending: fetchPending } = useFetchMangaChapters({
mutation: {
onSuccess: () => queryClient.invalidateQueries({ queryKey }),
},
});
const { mutate: fetchAllMutate, isPending: fetchAllPending } = useFetchAllChapters({
mutation: {
onSuccess: () => toast.success("Chapter import queued successfully.")
}
})
const isPending = fetchPending || fetchAllPending;
const [openProviders, setOpenProviders] = useState<Set<number>>(new Set());
if (!mangaData) {
@ -119,7 +128,7 @@ export default function MangaDetailPage() {
<Image
src={
(mangaData.data?.coverImageKey &&
"http://omv.badger-pirarucu.ts.net:9000/mangamochi/" +
process.env.NEXT_PUBLIC_OMV_BASE_URL + "/" +
mangaData.data?.coverImageKey) ||
"/placeholder.svg"
}
@ -258,7 +267,19 @@ export default function MangaDetailPage() {
</div>
</div>
{provider.supportsChapterFetch && (
<div className={"pr-4"}>
<div className={"flex gap-4 pr-4"}>
<Button
size="sm"
variant="outline"
disabled={isPending}
onClick={() =>
fetchAllMutate({ mangaProviderId: provider.id })
}
className="gap-2"
>
<Database className="h-4 w-4" />
Fetch all from Provider
</Button>
<Button
size="sm"
variant="outline"

View File

@ -110,7 +110,7 @@ export function MangaCard({ manga, queryKey }: MangaCardProps) {
<Image
src={
(manga.coverImageKey &&
"http://omv.badger-pirarucu.ts.net:9000/mangamochi/" +
process.env.NEXT_PUBLIC_OMV_BASE_URL + "/" +
manga.coverImageKey) ||
"/placeholder.svg"
}

View File

@ -7,7 +7,6 @@ module.exports = {
target: "api/mangamochi.ts",
client: "react-query",
httpClient: "axios",
baseUrl: "http://mangamochi.badger-pirarucu.ts.net:8080",
urlEncodeParameters: true,
override: {
mutator: {