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 403a45c..e2c7409 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -86,6 +86,7 @@ paths: description: Unknown server error '/titles/{title_id}': get: + operationId: getTitle summary: Get title description parameters: - name: title_id @@ -116,6 +117,7 @@ paths: description: Unknown server error '/users/{user_id}': get: + operationId: getUsersId summary: Get user info parameters: - name: user_id 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 18c805e..2cff448 100644 --- a/api/paths/users-id-titles.yaml +++ b/api/paths/users-id-titles.yaml @@ -128,7 +128,6 @@ post: application/json: schema: $ref: '../schemas/UserTitleMini.yaml' - '400': description: Invalid request body (missing fields, invalid types, etc.) '401': @@ -180,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 e20bcbf..e1a5a74 100644 --- a/api/schemas/UserTitleMini.yaml +++ b/api/schemas/UserTitleMini.yaml @@ -20,4 +20,4 @@ properties: format: int64 ctime: type: string - format: date-time \ No newline at end of file + format: date-time diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go index 2cf2283..f820db6 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -1,7 +1,6 @@ package handlers import ( - "context" "encoding/json" "fmt" oapi "nyanimedb/api" @@ -17,11 +16,11 @@ func NewServer(db *sqlc.Queries) Server { return Server{db: db} } -func sql2StorageType(s *sqlc.StorageTypeT) (*oapi.ImageStorageType, error) { +func sql2StorageType(s *sqlc.StorageTypeT) (*oapi.StorageType, error) { if s == nil { return nil, nil } - var t oapi.ImageStorageType + var t oapi.StorageType switch *s { case sqlc.StorageTypeTLocal: t = oapi.Local @@ -33,7 +32,7 @@ func sql2StorageType(s *sqlc.StorageTypeT) (*oapi.ImageStorageType, error) { return &t, nil } -func (s Server) mapTitle(ctx context.Context, title sqlc.GetTitleByIDRow) (oapi.Title, error) { +func (s Server) mapTitle(title sqlc.GetTitleByIDRow) (oapi.Title, error) { oapi_title := oapi.Title{ EpisodesAired: title.EpisodesAired, diff --git a/modules/backend/handlers/titles.go b/modules/backend/handlers/titles.go index c67177f..03553fd 100644 --- a/modules/backend/handlers/titles.go +++ b/modules/backend/handlers/titles.go @@ -144,7 +144,7 @@ func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitl return oapi.GetTitlesTitleId500Response{}, nil } - oapi_title, err = s.mapTitle(ctx, sqlc_title) + oapi_title, err = s.mapTitle(sqlc_title) if err != nil { log.Errorf("%v", err) return oapi.GetTitlesTitleId500Response{}, nil @@ -238,7 +238,7 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje // _title.TitleStorageType = string(s) // } - t, err := s.mapTitle(ctx, _title) + t, err := s.mapTitle(_title) if err != nil { log.Errorf("%v", err) return oapi.GetTitles500Response{}, nil diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index 1881f36..7af705e 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "errors" "fmt" oapi "nyanimedb/api" sqlc "nyanimedb/sql" @@ -9,24 +10,12 @@ import ( "time" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgtype" "github.com/oapi-codegen/runtime/types" log "github.com/sirupsen/logrus" ) -// type Server struct { -// db *sqlc.Queries -// } - -// func NewServer(db *sqlc.Queries) Server { -// return Server{db: db} -// } - -// func parseInt64(s string) (int32, error) { -// i, err := strconv.ParseInt(s, 10, 64) -// return int32(i), err -// } - func mapUser(u sqlc.GetUserByIDRow) (oapi.User, error) { i := oapi.Image{ Id: u.AvatarID, @@ -202,7 +191,7 @@ func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (o // StudioImagePath: title.StudioImagePath, } - oapi_title, err := s.mapTitle(ctx, _title) + oapi_title, err := s.mapTitle(_title) if err != nil { return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err) } @@ -368,19 +357,26 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque } params := sqlc.InsertUserTitleParams{ - UserID: request.UserId, - TitleID: request.Body.TitleId, - Status: *status, - Rate: request.Body.Rate, - ReviewID: request.Body.ReviewId, + UserID: request.UserId, + TitleID: request.Body.TitleId, + Status: *status, + Rate: request.Body.Rate, } user_title, err := s.db.InsertUserTitle(ctx, params) if err != nil { - log.Errorf("%v", err) - return oapi.AddUserTitle500Response{}, nil + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + // fmt.Println(pgErr.Message) // => syntax error at end of input + // fmt.Println(pgErr.Code) // => 42601 + if pgErr.Code == "23505" { //duplicate key value + return oapi.AddUserTitle409Response{}, nil + } + } else { + log.Errorf("%v", err) + return oapi.AddUserTitle500Response{}, nil + } } - oapi_status, err := sql2usertitlestatus(user_title.Status) if err != nil { log.Errorf("%v", err) @@ -406,3 +402,13 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque return oapi.AddUserTitle200JSONResponse(oapi_usertitle), nil } + +// DeleteUserTitle implements oapi.StrictServerInterface. +func (s Server) DeleteUserTitle(ctx context.Context, request oapi.DeleteUserTitleRequestObject) (oapi.DeleteUserTitleResponseObject, error) { + panic("unimplemented") +} + +// UpdateUserTitle implements oapi.StrictServerInterface. +func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitleRequestObject) (oapi.UpdateUserTitleResponseObject, error) { + panic("unimplemented") +} diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 0146b25..ef6e26d 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -461,21 +461,13 @@ VALUES ( ) RETURNING user_id, title_id, status, rate, review_id, ctime; --- -- name: UpdateUserTitle :one --- UPDATE usertitles --- SET --- status = COALESCE(sqlc.narg('status'), status), --- rate = COALESCE(sqlc.narg('rate'), rate), --- review_id = COALESCE(sqlc.narg('review_id'), review_id) --- WHERE user_id = $1 AND title_id = $2 --- RETURNING *; - --- -- name: DeleteUserTitle :exec --- DELETE FROM usertitles --- WHERE user_id = $1 AND ($2::int IS NULL OR title_id = $2); - --- -- name: ListTags :many --- SELECT tag_id, tag_names --- FROM tags --- ORDER BY tag_id --- LIMIT $1 OFFSET $2; \ No newline at end of file +-- name: UpdateUserTitle :one +-- Fails with sql.ErrNoRows if (user_id, title_id) not found +UPDATE usertitles +SET + status = COALESCE(sqlc.narg('status')::usertitle_status_t, status), + rate = COALESCE(sqlc.narg('rate')::int, rate) +WHERE + user_id = sqlc.arg('user_id') + AND title_id = sqlc.arg('title_id') +RETURNING *; \ No newline at end of file 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) { diff --git a/sql/migrations/000001_init.up.sql b/sql/migrations/000001_init.up.sql index f8781de..3499fe2 100644 --- a/sql/migrations/000001_init.up.sql +++ b/sql/migrations/000001_init.up.sql @@ -179,6 +179,6 @@ END; $$ LANGUAGE plpgsql; CREATE TRIGGER set_ctime_on_update -AFTER UPDATE ON usertitles +BEFORE UPDATE ON usertitles FOR EACH ROW EXECUTE FUNCTION set_ctime(); \ No newline at end of file diff --git a/sql/queries.sql.go b/sql/queries.sql.go index a46da86..89b16c9 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -925,3 +925,41 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateU ) return i, err } + +const updateUserTitle = `-- name: UpdateUserTitle :one +UPDATE usertitles +SET + status = COALESCE($1::usertitle_status_t, status), + rate = COALESCE($2::int, rate) +WHERE + user_id = $3 + AND title_id = $4 +RETURNING user_id, title_id, status, rate, review_id, ctime +` + +type UpdateUserTitleParams struct { + Status NullUsertitleStatusT `json:"status"` + Rate *int32 `json:"rate"` + UserID int64 `json:"user_id"` + TitleID int64 `json:"title_id"` +} + +// Fails with sql.ErrNoRows if (user_id, title_id) not found +func (q *Queries) UpdateUserTitle(ctx context.Context, arg UpdateUserTitleParams) (Usertitle, error) { + row := q.db.QueryRow(ctx, updateUserTitle, + arg.Status, + arg.Rate, + arg.UserID, + arg.TitleID, + ) + var i Usertitle + err := row.Scan( + &i.UserID, + &i.TitleID, + &i.Status, + &i.Rate, + &i.ReviewID, + &i.Ctime, + ) + return i, err +}