diff --git a/.forgejo/workflows/build-and-deploy.yml b/.forgejo/workflows/build-and-deploy.yml index e7d0a83..0338440 100644 --- a/.forgejo/workflows/build-and-deploy.yml +++ b/.forgejo/workflows/build-and-deploy.yml @@ -20,9 +20,9 @@ jobs: go-version: '^1.25' check-latest: false cache-dependency-path: | - modules/backend/go.sum + go.sum - - name: Build Go app + - name: Build backend run: | cd modules/backend go mod tidy @@ -35,6 +35,19 @@ jobs: name: nyanimedb-backend.tar.gz path: modules/backend/nyanimedb-backend.tar.gz + - name: Build auth + run: | + cd modules/auth + go mod tidy + go build -o auth . + tar -czvf nyanimedb-auth.tar.gz auth + + - name: Upload built auth to artifactory + uses: actions/upload-artifact@v3 + with: + name: nyanimedb-auth.tar.gz + path: modules/auth/nyanimedb-auth.tar.gz + # Build frontend - uses: actions/setup-node@v5 with: @@ -76,6 +89,14 @@ jobs: push: true tags: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest + - name: Build and push auth image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfiles/Dockerfile_auth + push: true + tags: meowgit.nekoea.red/nihonium/nyanimedb-auth:latest + - name: Build and push frontend image uses: docker/build-push-action@v6 with: diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index e7482c1..720b686 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -11,52 +11,52 @@ paths: parameters: - $ref: '#/components/parameters/cursor' - $ref: '#/components/parameters/title_sort' - - in: query - name: sort_forward + - name: sort_forward + in: query schema: type: boolean default: true - - in: query - name: word + - name: word + in: query schema: type: string - - in: query - name: status + - name: status + in: query + description: List of title statuses to filter schema: type: array items: $ref: '#/components/schemas/TitleStatus' - description: List of title statuses to filter - style: form explode: false - - in: query - name: rating + style: form + - name: rating + in: query schema: type: number format: double - - in: query - name: release_year + - name: release_year + in: query schema: type: integer format: int32 - - in: query - name: release_season + - name: release_season + in: query schema: $ref: '#/components/schemas/ReleaseSeason' - - in: query - name: limit + - name: limit + in: query schema: type: integer format: int32 default: 10 - - in: query - name: offset + - name: offset + in: query schema: type: integer format: int32 default: 0 - - in: query - name: fields + - name: fields + in: query schema: type: string default: all @@ -69,10 +69,10 @@ paths: type: object properties: data: + description: List of titles type: array items: $ref: '#/components/schemas/Title' - description: List of titles cursor: $ref: '#/components/schemas/CursorObj' required: @@ -86,16 +86,17 @@ paths: description: Unknown server error '/titles/{title_id}': get: + operationId: getTitle summary: Get title description parameters: - - in: path - name: title_id + - name: title_id + in: path required: true schema: type: integer format: int64 - - in: query - name: fields + - name: fields + in: query schema: type: string default: all @@ -116,15 +117,16 @@ paths: description: Unknown server error '/users/{user_id}': get: + operationId: getUsersId summary: Get user info parameters: - - in: path - name: user_id + - name: user_id + in: path required: true schema: type: string - - in: query - name: fields + - name: fields + in: query schema: type: string default: all @@ -142,59 +144,59 @@ paths: '500': description: Unknown server error patch: + operationId: updateUser summary: Partially update a user account description: | Update selected user profile fields (excluding password). Password updates must be done via the dedicated auth-service (`/auth/`). Fields not provided in the request body remain unchanged. - operationId: updateUser parameters: - name: user_id in: path + description: User ID (primary key) required: true schema: type: integer format: int64 - description: User ID (primary key) example: 123 requestBody: required: true content: application/json: schema: + description: Only provided fields are updated. Omitted fields remain unchanged. type: object properties: avatar_id: + description: ID of the user avatar (references `images.id`); set to `null` to remove avatar type: integer format: int64 - nullable: true - description: ID of the user avatar (references `images.id`); set to `null` to remove avatar example: 42 + nullable: true mail: + description: User email (must be unique and valid) type: string format: email - pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' - description: User email (must be unique and valid) example: john.doe.updated@example.com + pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' nickname: - type: string - pattern: '^[a-zA-Z0-9_-]{3,16}$' description: 'Username (alphanumeric + `_` or `-`, 3–16 chars)' + type: string + example: john_doe_43 maxLength: 16 minLength: 3 - example: john_doe_43 + pattern: '^[a-zA-Z0-9_-]{3,16}$' disp_name: - type: string description: Display name - maxLength: 32 - example: John Smith - user_desc: type: string + example: John Smith + maxLength: 32 + user_desc: description: User description / bio - maxLength: 512 + type: string example: Just a curious developer. + maxLength: 512 additionalProperties: false - description: Only provided fields are updated. Omitted fields remain unchanged. responses: '200': description: User updated successfully. Returns updated user representation (excluding sensitive fields). @@ -222,64 +224,64 @@ paths: parameters: - $ref: '#/components/parameters/cursor' - $ref: '#/components/parameters/title_sort' - - in: path - name: user_id + - name: user_id + in: path required: true schema: type: string - - in: query - name: sort_forward + - name: sort_forward + in: query schema: type: boolean default: true - - in: query - name: word + - name: word + in: query schema: type: string - - in: query - name: status + - name: status + in: query + description: List of title statuses to filter schema: type: array items: $ref: '#/components/schemas/TitleStatus' - description: List of title statuses to filter - style: form explode: false - - in: query - name: watch_status + style: form + - name: watch_status + in: query schema: type: array items: $ref: '#/components/schemas/UserTitleStatus' - style: form explode: false - - in: query - name: rating + style: form + - name: rating + in: query schema: type: number format: double - - in: query - name: my_rate + - name: my_rate + in: query schema: type: integer format: int32 - - in: query - name: release_year + - name: release_year + in: query schema: type: integer format: int32 - - in: query - name: release_season + - name: release_season + in: query schema: $ref: '#/components/schemas/ReleaseSeason' - - in: query - name: limit + - name: limit + in: query schema: type: integer format: int32 default: 10 - - in: query - name: fields + - name: fields + in: query schema: type: string default: all @@ -309,17 +311,17 @@ paths: '500': description: Unknown server error post: + operationId: addUserTitle summary: Add a title to a user description: 'User adding title to list af watched, status required' - operationId: addUserTitle parameters: - name: user_id in: path + description: ID of the user to assign the title to required: true schema: type: integer format: int64 - description: ID of the user to assign the title to example: 123 requestBody: required: true @@ -327,9 +329,6 @@ paths: application/json: schema: type: object - required: - - title_id - - status properties: title_id: type: integer @@ -339,36 +338,16 @@ paths: rate: type: integer format: int32 + required: + - title_id + - status responses: '200': description: Title successfully added to user content: application/json: schema: - type: object - required: - - user_id - - title_id - - status - properties: - user_id: - type: integer - format: int64 - title_id: - type: integer - format: int64 - status: - $ref: '#/components/schemas/UserTitleStatus' - rate: - type: integer - format: int32 - review_id: - type: integer - format: int64 - ctime: - type: string - format: date-time - additionalProperties: false + $ref: '#/components/schemas/UserTitleMini' '400': description: 'Invalid request body (missing fields, invalid types, etc.)' '401': @@ -382,17 +361,17 @@ paths: '500': description: Internal server error patch: + operationId: updateUserTitle summary: Update a usertitle description: User updating title list of watched - operationId: updateUserTitle parameters: - name: user_id in: path + description: ID of the user to assign the title to required: true schema: type: integer format: int64 - description: ID of the user to assign the title to example: 123 requestBody: required: true @@ -400,8 +379,6 @@ paths: application/json: schema: type: object - required: - - title_id properties: title_id: type: integer @@ -411,13 +388,15 @@ paths: rate: type: integer format: int32 + required: + - title_id responses: '200': description: Title successfully updated content: application/json: schema: - $ref: '#/paths/~1users~1%7Buser_id%7D~1titles/post/responses/200/content/application~1json/schema' + $ref: '#/components/schemas/UserTitleMini' '400': description: 'Invalid request body (missing fields, invalid types, etc.)' '401': @@ -443,25 +422,36 @@ components: schema: $ref: '#/components/schemas/TitleSort' schemas: - CursorObj: - type: object - required: - - id - properties: - id: - type: integer - format: int64 - param: - type: string TitleSort: - type: string description: Title sort order + type: string default: id enum: - id - year - rating - views + TitleStatus: + description: Title status + type: string + enum: + - finished + - ongoing + - planned + ReleaseSeason: + description: Title release season + type: string + enum: + - winter + - spring + - summer + - fall + StorageType: + description: Image storage type + type: string + enum: + - s3 + - local Image: type: object properties: @@ -469,65 +459,11 @@ components: type: integer format: int64 storage_type: - type: string - description: Image storage type - enum: - - s3 - - local + $ref: '#/components/schemas/StorageType' image_path: type: string - TitleStatus: - type: string - description: Title status - enum: - - finished - - ongoing - - planned - ReleaseSeason: - type: string - description: Title release season - enum: - - winter - - spring - - summer - - fall - UserTitleStatus: - type: string - description: User's title status - enum: - - finished - - planned - - dropped - - in-progress - Review: - type: object - additionalProperties: true - Tag: - type: object - description: 'A localized tag: keys are language codes (ISO 639-1), values are tag names' - additionalProperties: - type: string - example: - en: Shojo - ru: Сёдзё - ja: 少女 - Tags: - type: array - description: Array of localized tags - items: - $ref: '#/components/schemas/Tag' - example: - - en: Shojo - ru: Сёдзё - ja: 少女 - - en: Shounen - ru: Сёнен - ja: 少年 Studio: type: object - required: - - id - - name properties: id: type: integer @@ -538,30 +474,41 @@ components: $ref: '#/components/schemas/Image' description: type: string - Title: - type: object required: - id - - title_names - - tags + - name + Tag: + description: 'A localized tag: keys are language codes (ISO 639-1), values are tag names' + type: object + example: + en: Shojo + ru: Сёдзё + ja: 少女 + additionalProperties: + type: string + Tags: + description: Array of localized tags + type: array + items: + $ref: '#/components/schemas/Tag' + example: + - en: Shojo + ru: Сёдзё + ja: 少女 + - en: Shounen + ru: Сёнен + ja: 少年 + Title: + type: object properties: id: + description: Unique title ID (primary key) type: integer format: int64 - description: Unique title ID (primary key) example: 1 title_names: - type: object description: 'Localized titles. Key = language (ISO 639-1), value = list of names' - additionalProperties: - type: array - items: - type: string - example: Attack on Titan - minItems: 1 - example: - - Attack on Titan - - AoT + type: object example: en: - Attack on Titan @@ -571,6 +518,15 @@ components: - Титаны ja: - 進撃の巨人 + additionalProperties: + type: array + items: + type: string + example: Attack on Titan + minItems: 1 + example: + - Attack on Titan + - AoT studio: $ref: '#/components/schemas/Studio' tags: @@ -601,51 +557,68 @@ components: additionalProperties: type: number format: double - additionalProperties: true - User: + required: + - id + - title_names + - tags + CursorObj: type: object properties: id: type: integer format: int64 + param: + type: string + required: + - id + User: + type: object + properties: + id: description: Unique user ID (primary key) + type: integer + format: int64 example: 1 image: $ref: '#/components/schemas/Image' mail: + description: User email type: string format: email - description: User email example: john.doe@example.com nickname: - type: string description: Username (alphanumeric + _ or -) - maxLength: 16 + type: string example: john_doe_42 + maxLength: 16 disp_name: - type: string description: Display name - maxLength: 32 - example: John Doe - user_desc: type: string + example: John Doe + maxLength: 32 + user_desc: description: User description - maxLength: 512 + type: string example: Just a regular user. + maxLength: 512 creation_date: + description: Timestamp when the user was created type: string format: date-time - description: Timestamp when the user was created example: '2025-10-10T23:45:47.908073Z' required: - user_id - nickname + UserTitleStatus: + description: User's title status + type: string + enum: + - finished + - planned + - dropped + - in-progress UserTitle: type: object - required: - - user_id - - title_id - - status properties: user_id: type: integer @@ -663,3 +636,34 @@ components: ctime: type: string format: date-time + required: + - user_id + - title_id + - status + UserTitleMini: + type: object + properties: + user_id: + type: integer + format: int64 + title_id: + type: integer + format: int64 + status: + $ref: '#/components/schemas/UserTitleStatus' + rate: + type: integer + format: int32 + review_id: + type: integer + format: int64 + ctime: + type: string + format: date-time + required: + - user_id + - title_id + - status + Review: + type: object + additionalProperties: true diff --git a/api/paths/titles-id.yaml b/api/paths/titles-id.yaml index 01fa504..235743f 100644 --- a/api/paths/titles-id.yaml +++ b/api/paths/titles-id.yaml @@ -1,5 +1,6 @@ get: summary: Get title description + operationId: getTitle parameters: - in: path name: title_id diff --git a/api/paths/users-id-titles.yaml b/api/paths/users-id-titles.yaml index 1580cc1..4f11ab6 100644 --- a/api/paths/users-id-titles.yaml +++ b/api/paths/users-id-titles.yaml @@ -117,11 +117,10 @@ post: type: integer format: int64 status: - $ref: ../schemas/enums/UserTitleStatus.yaml + $ref: '../schemas/enums/UserTitleStatus.yaml' rate: type: integer format: int32 - responses: '200': description: Title successfully added to user @@ -129,7 +128,6 @@ post: application/json: schema: $ref: '../schemas/UserTitleMini.yaml' - '400': description: Invalid request body (missing fields, invalid types, etc.) '401': @@ -169,7 +167,7 @@ patch: type: integer format: int64 status: - $ref: ../schemas/enums/UserTitleStatus.yaml + $ref: '../schemas/enums/UserTitleStatus.yaml' rate: type: integer format: int32 @@ -181,7 +179,6 @@ patch: application/json: schema: $ref: '../schemas/UserTitleMini.yaml' - '400': description: Invalid request body (missing fields, invalid types, etc.) '401': diff --git a/api/paths/users-id.yaml b/api/paths/users-id.yaml index 06f4a19..fe62e46 100644 --- a/api/paths/users-id.yaml +++ b/api/paths/users-id.yaml @@ -1,5 +1,6 @@ get: summary: Get user info + operationId: getUsersId parameters: - in: path name: user_id diff --git a/api/schemas/Title.yaml b/api/schemas/Title.yaml index 7497d1f..877ee24 100644 --- a/api/schemas/Title.yaml +++ b/api/schemas/Title.yaml @@ -60,4 +60,3 @@ properties: additionalProperties: type: number format: double -additionalProperties: true diff --git a/api/schemas/UserTitleMini.yaml b/api/schemas/UserTitleMini.yaml index 9e45e95..e1a5a74 100644 --- a/api/schemas/UserTitleMini.yaml +++ b/api/schemas/UserTitleMini.yaml @@ -21,4 +21,3 @@ properties: ctime: type: string format: date-time -additionalProperties: false diff --git a/modules/frontend/src/App.tsx b/modules/frontend/src/App.tsx index 3ecfa2d..e2c909f 100644 --- a/modules/frontend/src/App.tsx +++ b/modules/frontend/src/App.tsx @@ -2,6 +2,7 @@ import React from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import UsersIdPage from "./pages/UsersIdPage/UsersIdPage"; import TitlesPage from "./pages/TitlesPage/TitlesPage"; +import TitlePage from "./pages/TitlePage/TitlePage"; import { LoginPage } from "./pages/LoginPage/LoginPage"; import { Header } from "./components/Header/Header"; @@ -24,7 +25,9 @@ const App: React.FC = () => { /> } /> + } /> + } /> ); diff --git a/modules/frontend/src/api/core/OpenAPI.ts b/modules/frontend/src/api/core/OpenAPI.ts index 185e5c3..6ce873e 100644 --- a/modules/frontend/src/api/core/OpenAPI.ts +++ b/modules/frontend/src/api/core/OpenAPI.ts @@ -20,7 +20,7 @@ export type OpenAPIConfig = { }; export const OpenAPI: OpenAPIConfig = { - BASE: '/api/v1', + BASE: 'http://10.1.0.65:8081/api/v1', VERSION: '1.0.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', diff --git a/modules/frontend/src/api/index.ts b/modules/frontend/src/api/index.ts index 80ae491..9013fc7 100644 --- a/modules/frontend/src/api/index.ts +++ b/modules/frontend/src/api/index.ts @@ -12,6 +12,7 @@ export type { CursorObj } from './models/CursorObj'; export type { Image } from './models/Image'; export type { ReleaseSeason } from './models/ReleaseSeason'; export type { Review } from './models/Review'; +export type { StorageType } from './models/StorageType'; export type { Studio } from './models/Studio'; export type { Tag } from './models/Tag'; export type { Tags } from './models/Tags'; @@ -21,6 +22,7 @@ export type { TitleSort } from './models/TitleSort'; export type { TitleStatus } from './models/TitleStatus'; export type { User } from './models/User'; export type { UserTitle } from './models/UserTitle'; +export type { UserTitleMini } from './models/UserTitleMini'; export type { UserTitleStatus } from './models/UserTitleStatus'; export { DefaultService } from './services/DefaultService'; diff --git a/modules/frontend/src/api/models/Image.ts b/modules/frontend/src/api/models/Image.ts index a94de74..887bf2f 100644 --- a/modules/frontend/src/api/models/Image.ts +++ b/modules/frontend/src/api/models/Image.ts @@ -2,12 +2,10 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { StorageType } from './StorageType'; export type Image = { id?: number; - /** - * Image storage type - */ - storage_type?: 's3' | 'local'; + storage_type?: StorageType; image_path?: string; }; diff --git a/modules/frontend/src/api/models/StorageType.ts b/modules/frontend/src/api/models/StorageType.ts new file mode 100644 index 0000000..f6d086b --- /dev/null +++ b/modules/frontend/src/api/models/StorageType.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * Image storage type + */ +export type StorageType = 's3' | 'local'; diff --git a/modules/frontend/src/api/models/Title.ts b/modules/frontend/src/api/models/Title.ts index 4da7aa3..9ffdeb6 100644 --- a/modules/frontend/src/api/models/Title.ts +++ b/modules/frontend/src/api/models/Title.ts @@ -2,4 +2,30 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export type Title = Record; +import type { Image } from './Image'; +import type { ReleaseSeason } from './ReleaseSeason'; +import type { Studio } from './Studio'; +import type { Tags } from './Tags'; +import type { TitleStatus } from './TitleStatus'; +export type Title = { + /** + * Unique title ID (primary key) + */ + id: number; + /** + * Localized titles. Key = language (ISO 639-1), value = list of names + */ + title_names: Record>; + studio?: Studio; + tags: Tags; + poster?: Image; + title_status?: TitleStatus; + rating?: number; + rating_count?: number; + release_year?: number; + release_season?: ReleaseSeason; + episodes_aired?: number; + episodes_all?: number; + episodes_len?: Record; +}; + diff --git a/modules/frontend/src/api/models/UserTitleMini.ts b/modules/frontend/src/api/models/UserTitleMini.ts new file mode 100644 index 0000000..2b223ce --- /dev/null +++ b/modules/frontend/src/api/models/UserTitleMini.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { UserTitleStatus } from './UserTitleStatus'; +export type UserTitleMini = { + user_id: number; + title_id: number; + status: UserTitleStatus; + rate?: number; + review_id?: number; + ctime?: string; +}; + diff --git a/modules/frontend/src/api/services/DefaultService.ts b/modules/frontend/src/api/services/DefaultService.ts index 874971e..5070fae 100644 --- a/modules/frontend/src/api/services/DefaultService.ts +++ b/modules/frontend/src/api/services/DefaultService.ts @@ -9,6 +9,7 @@ import type { TitleSort } from '../models/TitleSort'; import type { TitleStatus } from '../models/TitleStatus'; import type { User } from '../models/User'; import type { UserTitle } from '../models/UserTitle'; +import type { UserTitleMini } from '../models/UserTitleMini'; import type { UserTitleStatus } from '../models/UserTitleStatus'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; @@ -78,7 +79,7 @@ export class DefaultService { * @returns Title Title description * @throws ApiError */ - public static getTitles1( + public static getTitle( titleId: number, fields: string = 'all', ): CancelablePromise { @@ -105,7 +106,7 @@ export class DefaultService { * @returns User User info * @throws ApiError */ - public static getUsers( + public static getUsersId( userId: string, fields: string = 'all', ): CancelablePromise<User> { @@ -248,22 +249,17 @@ export class DefaultService { * User adding title to list af watched, status required * @param userId ID of the user to assign the title to * @param requestBody - * @returns any Title successfully added to user + * @returns UserTitleMini Title successfully added to user * @throws ApiError */ public static addUserTitle( userId: number, - requestBody: UserTitle, - ): CancelablePromise<{ - data?: { - user_id: number; + requestBody: { title_id: number; status: UserTitleStatus; rate?: number; - review_id?: number; - ctime?: string; - }; - }> { + }, + ): CancelablePromise<UserTitleMini> { return __request(OpenAPI, { method: 'POST', url: '/users/{user_id}/titles', @@ -282,4 +278,37 @@ export class DefaultService { }, }); } + /** + * Update a usertitle + * User updating title list of watched + * @param userId ID of the user to assign the title to + * @param requestBody + * @returns UserTitleMini Title successfully updated + * @throws ApiError + */ + public static updateUserTitle( + userId: number, + requestBody: { + title_id: number; + status?: UserTitleStatus; + rate?: number; + }, + ): CancelablePromise<UserTitleMini> { + return __request(OpenAPI, { + method: 'PATCH', + url: '/users/{user_id}/titles', + path: { + 'user_id': userId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid request body (missing fields, invalid types, etc.)`, + 401: `Unauthorized — missing or invalid auth token`, + 403: `Forbidden — user not allowed to update title`, + 404: `User or Title not found`, + 500: `Internal server error`, + }, + }); + } } diff --git a/modules/frontend/src/auth/core/OpenAPI.ts b/modules/frontend/src/auth/core/OpenAPI.ts index 2d0edf8..79aa305 100644 --- a/modules/frontend/src/auth/core/OpenAPI.ts +++ b/modules/frontend/src/auth/core/OpenAPI.ts @@ -20,7 +20,7 @@ export type OpenAPIConfig = { }; export const OpenAPI: OpenAPIConfig = { - BASE: '/auth', + BASE: 'http://10.1.0.65:8081/auth', VERSION: '1.0.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', diff --git a/modules/frontend/src/pages/TitlePage/TitlePage.module.css b/modules/frontend/src/pages/TitlePage/TitlePage.module.css deleted file mode 100644 index e69de29..0000000 diff --git a/modules/frontend/src/pages/TitlePage/TitlePage.tsx b/modules/frontend/src/pages/TitlePage/TitlePage.tsx index 7fe9de7..5ea0e3d 100644 --- a/modules/frontend/src/pages/TitlePage/TitlePage.tsx +++ b/modules/frontend/src/pages/TitlePage/TitlePage.tsx @@ -1,64 +1,140 @@ -// import React, { useEffect, useState } from "react"; -// import { useParams } from "react-router-dom"; -// import { DefaultService } from "../../api/services/DefaultService"; -// import type { User } from "../../api/models/User"; -// import styles from "./UserPage.module.css"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { DefaultService } from "../../api/services/DefaultService"; +import type { Title, UserTitleStatus } from "../../api"; +import { + ClockIcon, + CheckCircleIcon, + PlayCircleIcon, + XCircleIcon, +} from "@heroicons/react/24/solid"; -// const UserPage: React.FC = () => { -// const { id } = useParams<{ id: string }>(); -// const [user, setUser] = useState<User | null>(null); -// const [loading, setLoading] = useState(true); -// const [error, setError] = useState<string | null>(null); +const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: string }[] = [ + { status: "planned", icon: <ClockIcon className="w-6 h-6" />, label: "Planned" }, + { status: "finished", icon: <CheckCircleIcon className="w-6 h-6" />, label: "Finished" }, + { status: "in-progress", icon: <PlayCircleIcon className="w-6 h-6" />, label: "In Progress" }, + { status: "dropped", icon: <XCircleIcon className="w-6 h-6" />, label: "Dropped" }, +]; -// useEffect(() => { -// if (!id) return; +export default function TitlePage() { + const params = useParams(); + const titleId = Number(params.id); -// const getTitleInfo = async () => { -// try { -// const userInfo = await DefaultService.getTitle(id, "all"); -// setUser(userInfo); -// } catch (err) { -// console.error(err); -// setError("Failed to fetch user info."); -// } finally { -// setLoading(false); -// } -// }; -// getTitleInfo(); -// }, [id]); + const [title, setTitle] = useState<Title | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); -// if (loading) return <div className={styles.loader}>Loading...</div>; -// if (error) return <div className={styles.error}>{error}</div>; -// if (!user) return <div className={styles.error}>User not found.</div>; + const [userStatus, setUserStatus] = useState<UserTitleStatus | null>(null); + const [updatingStatus, setUpdatingStatus] = useState(false); -// return ( -// <div className={styles.container}> -// <div className={styles.card}> -// <div className={styles.avatar}> -// {user.avatar_id ? ( -// <img -// src={`/images/${user.avatar_id}.png`} -// alt="User Avatar" -// className={styles.avatarImg} -// /> -// ) : ( -// <div className={styles.avatarPlaceholder}> -// {user.disp_name?.[0] || "U"} -// </div> -// )} -// </div> + useEffect(() => { + const fetchTitle = async () => { + setLoading(true); + try { + const data = await DefaultService.getTitle(titleId, "all"); + setTitle(data); + setError(null); + } catch (err: any) { + console.error(err); + setError(err?.message || "Failed to fetch title"); + } finally { + setLoading(false); + } + }; + fetchTitle(); + }, [titleId]); -// <div className={styles.info}> -// <h1 className={styles.name}>{user.disp_name || user.nickname}</h1> -// <p className={styles.nickname}>@{user.nickname}</p> -// {user.user_desc && <p className={styles.desc}>{user.user_desc}</p>} -// <p className={styles.created}> -// Joined: {new Date(user.creation_date).toLocaleDateString()} -// </p> -// </div> -// </div> -// </div> -// ); -// }; + const handleStatusClick = async (status: UserTitleStatus) => { + if (updatingStatus || userStatus === status) return; -// export default UserPage; + const userId = Number(localStorage.getItem("userId")); + if (!userId) { + alert("You must be logged in to set status."); + return; + } + + setUpdatingStatus(true); + try { + await DefaultService.addUserTitle(userId, { + title_id: titleId, + status, + }); + setUserStatus(status); + } catch (err: any) { + console.error(err); + alert(err?.message || "Failed to set status"); + } finally { + setUpdatingStatus(false); + } + }; + + const getTagsString = () => + title?.tags?.map(tag => tag.en).filter(Boolean).join(", "); + + if (loading) return <div className="mt-20 font-medium text-black">Loading title...</div>; + if (error) return <div className="mt-20 text-red-600 font-medium">{error}</div>; + if (!title) return null; + + return ( + <div className="w-full min-h-screen bg-gray-50 p-6 flex justify-center"> + <div className="flex flex-col md:flex-row bg-white shadow-lg rounded-xl max-w-4xl w-full p-6 gap-6"> + {/* Постер */} + <div className="flex flex-col items-center"> + <img + src={title.poster?.image_path || "/default-poster.png"} + alt={title.title_names?.en?.[0] || "Title poster"} + className="w-48 h-72 object-cover rounded-lg mb-4" + /> + + {/* Статус кнопки с иконками */} + <div className="flex gap-2 mt-2 flex-wrap justify-center"> + {STATUS_BUTTONS.map(btn => ( + <button + key={btn.status} + onClick={() => handleStatusClick(btn.status)} + disabled={updatingStatus} + className={`p-2 rounded-lg transition flex items-center justify-center ${ + userStatus === btn.status + ? "bg-blue-600 text-white" + : "bg-gray-200 text-gray-700 hover:bg-gray-300" + }`} + title={btn.label} + > + {btn.icon} + </button> + ))} + </div> + </div> + + {/* Информация о тайтле */} + <div className="flex-1 flex flex-col"> + <h1 className="text-3xl font-bold mb-2"> + {title.title_names?.en?.[0] || "Untitled"} + </h1> + {title.studio && <p className="text-gray-700 mb-1">Studio: {title.studio.name}</p>} + {title.title_status && <p className="text-gray-700 mb-1">Status: {title.title_status}</p>} + {title.rating !== undefined && ( + <p className="text-gray-700 mb-1"> + Rating: {title.rating} ({title.rating_count} votes) + </p> + )} + {title.release_year && ( + <p className="text-gray-700 mb-1"> + Released: {title.release_year} {title.release_season || ""} + </p> + )} + {title.episodes_aired !== undefined && ( + <p className="text-gray-700 mb-1"> + Episodes: {title.episodes_aired}/{title.episodes_all} + </p> + )} + {title.tags && title.tags.length > 0 && ( + <p className="text-gray-700 mb-1"> + Tags: {getTagsString()} + </p> + )} + </div> + </div> + </div> + ); +} diff --git a/modules/frontend/src/pages/UserPage/UserPage.tsx b/modules/frontend/src/pages/UserPage/UserPage.tsx index 2e39e6b..eafdf6b 100644 --- a/modules/frontend/src/pages/UserPage/UserPage.tsx +++ b/modules/frontend/src/pages/UserPage/UserPage.tsx @@ -15,7 +15,7 @@ const UserPage: React.FC = () => { const getUserInfo = async () => { try { - const userInfo = await DefaultService.getUsers(id, "all"); // <-- use dynamic id + const userInfo = await DefaultService.getUsersId(id, "all"); // <-- use dynamic id setUser(userInfo); } catch (err) { console.error(err); diff --git a/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx b/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx index 342f22c..729da20 100644 --- a/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx +++ b/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx @@ -41,7 +41,7 @@ export default function UsersIdPage({ userId }: UsersIdPageProps) { if (!id) return; setLoadingUser(true); try { - const result = await DefaultService.getUsers(id, "all"); + const result = await DefaultService.getUsersId(id, "all"); setUser(result); setErrorUser(null); } catch (err: any) {