diff --git a/api/album_create.go b/api/album_create.go new file mode 100644 index 0000000000000000000000000000000000000000..2a39c8f453733ea0fc0cdcd1e3664c13475cf0d1 --- /dev/null +++ b/api/album_create.go @@ -0,0 +1,44 @@ +package api + +import ( + "encoding/json" + "git.kuschku.de/justjanne/imghost-frontend/auth" + "git.kuschku.de/justjanne/imghost-frontend/environment" + "git.kuschku.de/justjanne/imghost-frontend/model" + "git.kuschku.de/justjanne/imghost-frontend/util" + "net/http" +) + +func CreateAlbum(env environment.FrontendEnvironment) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + var err error + user, err := auth.ParseUser(request, env) + if err != nil { + http.Error(writer, err.Error(), http.StatusUnauthorized) + return + } + println("parsed user: " + user.Name) + + var data model.Album + err = json.NewDecoder(request.Body).Decode(&data) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + + data.Id = generateId() + data.Owner = user.Id + err = env.Repositories.Albums.Create(data) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + + album, err := env.Repositories.Albums.Get(data.Id) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + util.ReturnJson(writer, album) + }) +} diff --git a/api/album_get.go b/api/album_get.go index 928b2b4377eeb0cf4d070cfb70cd21c047ea7f1b..b8a9208dce428e7aef3ddbc99be033c3b0461ac8 100644 --- a/api/album_get.go +++ b/api/album_get.go @@ -30,6 +30,11 @@ func GetAlbum(env environment.FrontendEnvironment) http.Handler { http.Error(writer, err.Error(), http.StatusInternalServerError) return } + image.Metadata, err = env.Repositories.ImageMetadata.List(image) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } album.Images[i] = image } diff --git a/api/album_list.go b/api/album_list.go index 74d0f4e46d7c12fb75402633774fa7dd63914a94..a63f49e281b787d63de7206193fc36c8ee07a41d 100644 --- a/api/album_list.go +++ b/api/album_list.go @@ -34,6 +34,11 @@ func ListAlbums(env environment.FrontendEnvironment) http.Handler { http.Error(writer, err.Error(), http.StatusInternalServerError) return } + image.Metadata, err = env.Repositories.ImageMetadata.List(image) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } album.Images[j] = image } albums[i] = album diff --git a/api/album_reorder.go b/api/album_reorder.go index 8b412a426306a96491ece60b0845f298d378ae2f..9b11d016592af61ac37cb74df20fe99000f905e4 100644 --- a/api/album_reorder.go +++ b/api/album_reorder.go @@ -23,7 +23,7 @@ func ReorderAlbum(env environment.FrontendEnvironment) http.Handler { return } - var changes []model.AlbumImage + var changes []model.Image err = json.NewDecoder(request.Body).Decode(&changes) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) @@ -32,8 +32,8 @@ func ReorderAlbum(env environment.FrontendEnvironment) http.Handler { for index, image := range changes { image.Album = album.Id - - err = env.Repositories.AlbumImages.Reorder(image, index) + position := index + err = env.Repositories.AlbumImages.Reorder(image, position) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return diff --git a/api/albumimage_create.go b/api/albumimage_create.go new file mode 100644 index 0000000000000000000000000000000000000000..cbe1c85029958b45e5c17b2c1b3e00ebf14eff98 --- /dev/null +++ b/api/albumimage_create.go @@ -0,0 +1,31 @@ +package api + +import ( + "encoding/json" + "git.kuschku.de/justjanne/imghost-frontend/environment" + "git.kuschku.de/justjanne/imghost-frontend/model" + "github.com/gorilla/mux" + "net/http" +) + +func CreateAlbumImage(env environment.FrontendEnvironment) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + + var data model.Image + err := json.NewDecoder(request.Body).Decode(&data) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + data.Album = vars["albumId"] + + err = env.Repositories.AlbumImages.Create(data) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + + writer.WriteHeader(http.StatusNoContent) + }) +} diff --git a/api/albumimage_update.go b/api/albumimage_update.go index 8ef9201f3d9237e28662c396b57d618a5a9bfb36..8617f1971d510a416cac13fc0a74b4edd04f5d81 100644 --- a/api/albumimage_update.go +++ b/api/albumimage_update.go @@ -25,7 +25,7 @@ func UpdateAlbumImage(env environment.FrontendEnvironment) http.Handler { return } - var changes model.AlbumImage + var changes model.Image err = json.NewDecoder(request.Body).Decode(&changes) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index c050e6eb05a90d7338d41facc4e26b33a8c00267..f5b71cb9ccbf983fb617e0fb57ff239a1c36a6aa 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -51,6 +51,9 @@ func main() { router.Handle( "/api/v1/albums", api.ListAlbums(env)).Methods(http.MethodGet, http.MethodOptions) + router.Handle( + "/api/v1/albums", + api.CreateAlbum(env)).Methods(http.MethodPost, http.MethodOptions) router.Handle( "/api/v1/albums/{albumId}", api.GetAlbum(env)).Methods(http.MethodGet, http.MethodOptions) @@ -68,6 +71,9 @@ func main() { router.Handle( "/api/v1/albums/{albumId}/images/{imageId}", api.UpdateAlbumImage(env)).Methods(http.MethodPost, http.MethodOptions) + router.Handle( + "/api/v1/albums/{albumId}/images", + api.CreateAlbumImage(env)).Methods(http.MethodPost, http.MethodOptions) router.Handle( "/api/v1/albums/{albumId}/images/{imageId}", api.DeleteAlbumImage(env)).Methods(http.MethodDelete, http.MethodOptions) diff --git a/model/album.go b/model/album.go index ba560d17bd1af1b80976117d2f6de84b3b12f85c..1f4ba5a2314d4a4c023148cae6687ffdcc71fd77 100644 --- a/model/album.go +++ b/model/album.go @@ -6,13 +6,13 @@ import ( ) type Album struct { - Id string `json:"id" db:"id"` - Owner string `json:"owner" db:"owner"` - Title string `json:"title" db:"title"` - Description string `json:"description" db:"description"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - Images []AlbumImage `json:"images"` + Id string `json:"id" db:"id"` + Owner string `json:"owner" db:"owner"` + Title string `json:"title" db:"title"` + Description string `json:"description" db:"description"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + Images []Image `json:"images"` } func (album Album) VerifyOwner(user User) error { diff --git a/model/album_image.go b/model/album_image.go deleted file mode 100644 index 5d3b09fdb1ef42bd29969b958fcf1541f68b5f0b..0000000000000000000000000000000000000000 --- a/model/album_image.go +++ /dev/null @@ -1,19 +0,0 @@ -package model - -import ( - "git.kuschku.de/justjanne/imghost-frontend/configuration" - "git.kuschku.de/justjanne/imghost-frontend/storage" -) - -type AlbumImage struct { - Album string `json:"album" db:"album"` - Image string `json:"image" db:"image"` - Title string `json:"title" db:"title"` - Description string `json:"description" db:"description"` - Url string `json:"url"` -} - -func (image *AlbumImage) LoadUrl(storage storage.Storage, config configuration.StorageConfiguration) (err error) { - image.Url = storage.UrlFor(config.ImageBucket, image.Image).String() - return -} diff --git a/model/image.go b/model/image.go index b43c7fd68cf621d4ce4d180b9d6b869b5711dd65..45b10d2631d26475a8f8dba263eba3b86661567b 100644 --- a/model/image.go +++ b/model/image.go @@ -8,6 +8,7 @@ import ( ) type Image struct { + Album string `json:"album,omitempty" db:"album"` Id string `json:"id" db:"id"` Owner string `json:"owner" db:"owner"` Title string `json:"title" db:"title"` diff --git a/repo/album_images.go b/repo/album_images.go index 56e3a481f38d2a428dad30eb46396d3d3ffabec6..b7b0ddac81705b5a3accb2dc1237691b04884cc9 100644 --- a/repo/album_images.go +++ b/repo/album_images.go @@ -19,26 +19,42 @@ type AlbumImages struct { func NewAlbumImageRepo(db *sqlx.DB) (repo AlbumImages, err error) { repo.db = db repo.queryList, err = db.PrepareNamed(` - SELECT album, - image, - title, - description + SELECT album_images.album, + images.id, + albums.owner, + album_images.title, + album_images.description, + albums.created_at, + albums.updated_at, + images.original_name, + images.type, + images.state FROM album_images - WHERE album = :albumId - ORDER BY position + JOIN albums ON album_images.album = albums.id + JOIN images ON album_images.image = images.id + WHERE album_images.album = :albumId + ORDER BY album_images.position `) if err != nil { return } repo.queryGet, err = db.PrepareNamed(` - SELECT album, - image, - title, - description + SELECT album_images.album, + images.id, + albums.owner, + album_images.title, + album_images.description, + albums.created_at, + albums.updated_at, + images.original_name, + images.type, + images.state FROM album_images - WHERE album = :albumId - AND image = :imageId - ORDER BY position + JOIN albums ON album_images.album = albums.id + JOIN images ON album_images.image = images.id + WHERE album_images.album = :albumId + AND album_images.image = :imageId + ORDER BY album_images.position `) if err != nil { return @@ -67,7 +83,7 @@ func NewAlbumImageRepo(db *sqlx.DB) (repo AlbumImages, err error) { repo.stmtDelete, err = db.PrepareNamed(` DELETE FROM album_images WHERE album = :albumId - AND image = :imageID + AND image = :imageId `) if err != nil { return @@ -92,7 +108,7 @@ func NewAlbumImageRepo(db *sqlx.DB) (repo AlbumImages, err error) { return repo, nil } -func (repo AlbumImages) List(albumId string) (images []model.AlbumImage, err error) { +func (repo AlbumImages) List(albumId string) (images []model.Image, err error) { rows, err := repo.queryList.Queryx(map[string]interface{}{ "albumId": albumId, }) @@ -100,7 +116,7 @@ func (repo AlbumImages) List(albumId string) (images []model.AlbumImage, err err return } for rows.Next() { - var image model.AlbumImage + var image model.Image err = rows.StructScan(&image) if err != nil { return @@ -110,7 +126,7 @@ func (repo AlbumImages) List(albumId string) (images []model.AlbumImage, err err return } -func (repo AlbumImages) Get(albumId string, imageId string) (image model.AlbumImage, err error) { +func (repo AlbumImages) Get(albumId string, imageId string) (image model.Image, err error) { err = repo.queryGet.Get(&image, map[string]interface{}{ "albumId": albumId, "imageId": imageId, @@ -118,46 +134,46 @@ func (repo AlbumImages) Get(albumId string, imageId string) (image model.AlbumIm return } -func (repo AlbumImages) Create(new model.AlbumImage) (err error) { +func (repo AlbumImages) Create(new model.Image) (err error) { _, err = repo.stmtCreate.Exec(map[string]interface{}{ "albumId": new.Album, - "imageId": new.Image, + "imageId": new.Id, "title": new.Title, "description": new.Description, }) return } -func (repo AlbumImages) Update(changed model.AlbumImage) (err error) { +func (repo AlbumImages) Update(changed model.Image) (err error) { _, err = repo.stmtUpdate.Exec(map[string]interface{}{ "albumId": changed.Album, - "imageId": changed.Image, + "imageId": changed.Id, "title": changed.Title, "description": changed.Description, }) return } -func (repo AlbumImages) Reorder(changed model.AlbumImage, position int) (err error) { +func (repo AlbumImages) Reorder(changed model.Image, position int) (err error) { _, err = repo.stmtReorder.Exec(map[string]interface{}{ "albumId": changed.Album, - "imageId": changed.Image, + "imageId": changed.Id, "position": position, }) return } -func (repo AlbumImages) Delete(changed model.AlbumImage) (err error) { +func (repo AlbumImages) Delete(changed model.Image) (err error) { _, err = repo.stmtDelete.Exec(map[string]interface{}{ "albumId": changed.Album, - "imageId": changed.Image, + "imageId": changed.Id, }) return } -func (repo AlbumImages) DeleteAll(changed model.AlbumImage) (err error) { +func (repo AlbumImages) DeleteAll(changed model.Album) (err error) { _, err = repo.stmtDeleteAll.Exec(map[string]interface{}{ - "albumId": changed.Album, + "albumId": changed.Id, }) return } diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 106dcbf68a9941f8120eb8dd5e193bfa3b3af935..467d54eadf78c4858c995f4358604627db19afd8 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,14 +1,15 @@ import './App.css'; -import {BaseUrlProvider} from './api/baseUrlContext'; +import {BaseUrlProvider} from './api/util/BaseUrlContext'; import {QueryClient, QueryClientProvider} from "react-query"; import {BrowserRouter, Link, Route} from "react-router-dom"; import {Redirect, Switch} from "react-router"; import {AppBar, Button, Toolbar, Typography} from "@material-ui/core"; -import {useErrorDisplay} from "./components/ErrorContext"; +import {useErrorDisplay} from "./components/error/ErrorContext"; import {AlbumListPage} from "./pages/AlbumListPage"; import {ImageDetailPage} from './pages/ImageDetailPage'; import {ImageListPage} from "./pages/ImageListPage"; import {UploadView} from "./components/UploadView"; +import {AlbumDetailPage} from "./pages/AlbumDetailPage"; const queryClient = new QueryClient(); @@ -62,6 +63,9 @@ export function App() { <Route path="/albums"> <AlbumListPage/> </Route> + <Route path="/a/:albumId"> + <AlbumDetailPage/> + </Route> <Redirect from="/" to="/images"/> </Switch> </ErrorWrapper> diff --git a/ui/src/api/model/AlbumCreate.ts b/ui/src/api/model/AlbumCreate.ts new file mode 100644 index 0000000000000000000000000000000000000000..0929bbc6f1c3022ba695e97fddc30d4369e78d8f --- /dev/null +++ b/ui/src/api/model/AlbumCreate.ts @@ -0,0 +1,4 @@ +export interface AlbumCreate { + title: string, + description: string +} diff --git a/ui/src/api/model/AlbumImage.ts b/ui/src/api/model/AlbumImage.ts index 787d4d9061d0340d5a694e5c6a36feed2ebc26dc..2e892695be11773e8c596d0d07b3918a9541568d 100644 --- a/ui/src/api/model/AlbumImage.ts +++ b/ui/src/api/model/AlbumImage.ts @@ -1,7 +1,5 @@ -export interface AlbumImage { - album: string, - image: string, - title: string, - description: string, - url: string, +import {Image} from "./Image"; + +export interface AlbumImage extends Image { + album: string } diff --git a/ui/src/api/model/AlbumImageCreate.ts b/ui/src/api/model/AlbumImageCreate.ts new file mode 100644 index 0000000000000000000000000000000000000000..0abb8f185939e5d1bb5508f1fd9b7fcbcc273fba --- /dev/null +++ b/ui/src/api/model/AlbumImageCreate.ts @@ -0,0 +1,6 @@ +export interface AlbumImageCreate { + album: string, + id: string, + title: string, + description: string +} diff --git a/ui/src/api/model/Image.ts b/ui/src/api/model/Image.ts index 710716b15e7110af39cc998c7df7edb07a3fe825..69ba9075ea70a3106db63cab884dc19b2a74e3d9 100644 --- a/ui/src/api/model/Image.ts +++ b/ui/src/api/model/Image.ts @@ -1,3 +1,5 @@ +import {ImageState} from "./ImageState"; + export interface Image { id: string, owner: string, @@ -13,11 +15,3 @@ export interface Image { }, url: string, } - -export enum ImageState { - CREATED = "created", - QUEUED = "queued", - IN_PROGRESS = "in_progress", - DONE = "done", - ERROR = "error" -} diff --git a/ui/src/api/model/ImageState.ts b/ui/src/api/model/ImageState.ts new file mode 100644 index 0000000000000000000000000000000000000000..bdc3aa7b429b6af1cfcb9e4969b98265f08397d1 --- /dev/null +++ b/ui/src/api/model/ImageState.ts @@ -0,0 +1,7 @@ +export enum ImageState { + CREATED = "created", + QUEUED = "queued", + IN_PROGRESS = "in_progress", + DONE = "done", + ERROR = "error" +} diff --git a/ui/src/api/useCreateAlbum.ts b/ui/src/api/useCreateAlbum.ts new file mode 100644 index 0000000000000000000000000000000000000000..26ea7a23227446341c937cd84fa9689e832b72a3 --- /dev/null +++ b/ui/src/api/useCreateAlbum.ts @@ -0,0 +1,21 @@ +import {useBaseUrl} from "./util/BaseUrlContext"; +import {useMutation, useQueryClient} from "react-query"; +import axios from "axios"; +import {Album} from "./model/Album"; +import {AlbumCreate} from "./model/AlbumCreate"; + +export const useCreateAlbum = () => { + const baseUrl = useBaseUrl(); + const queryClient = useQueryClient(); + return useMutation<Album, unknown, AlbumCreate>((album: AlbumCreate) => axios.post( + `api/v1/albums`, + album, + { + baseURL: baseUrl + } + ), { + onSuccess: () => { + queryClient.invalidateQueries('album') + }, + }) +} diff --git a/ui/src/api/useCreateAlbumImage.ts b/ui/src/api/useCreateAlbumImage.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b3e3371e5931a3fe75b6c51c596653312bae1c7 --- /dev/null +++ b/ui/src/api/useCreateAlbumImage.ts @@ -0,0 +1,20 @@ +import {useBaseUrl} from "./util/BaseUrlContext"; +import {useMutation, useQueryClient} from "react-query"; +import axios from "axios"; +import {AlbumImageCreate} from "./model/AlbumImageCreate"; + +export const useCreateAlbumImage = () => { + const baseUrl = useBaseUrl(); + const queryClient = useQueryClient(); + return useMutation<void, unknown, AlbumImageCreate>((albumImage: AlbumImageCreate) => axios.post( + `api/v1/albums/${albumImage.album}/images`, + albumImage, + { + baseURL: baseUrl + } + ), { + onSuccess: () => { + queryClient.invalidateQueries('album') + }, + }) +} diff --git a/ui/src/api/useDeleteAlbum.ts b/ui/src/api/useDeleteAlbum.ts new file mode 100644 index 0000000000000000000000000000000000000000..d344e8f8c272c1cc7f49b1bb521f6d496cf0cfef --- /dev/null +++ b/ui/src/api/useDeleteAlbum.ts @@ -0,0 +1,19 @@ +import {useBaseUrl} from "./util/BaseUrlContext"; +import {useMutation, useQueryClient} from "react-query"; +import axios from "axios"; +import {Album} from "./model/Album"; + +export const useDeleteAlbum = () => { + const baseUrl = useBaseUrl(); + const queryClient = useQueryClient(); + return useMutation<void, unknown, Album>((album: Album) => axios.delete( + `api/v1/albums/${album.id}`, + { + baseURL: baseUrl + } + ), { + onSuccess: () => { + queryClient.invalidateQueries('album') + }, + }) +} diff --git a/ui/src/api/useDeleteAlbumImage.ts b/ui/src/api/useDeleteAlbumImage.ts new file mode 100644 index 0000000000000000000000000000000000000000..51e4a329b6c2e8e80d87bfa021f46b5557523378 --- /dev/null +++ b/ui/src/api/useDeleteAlbumImage.ts @@ -0,0 +1,19 @@ +import {useBaseUrl} from "./util/BaseUrlContext"; +import {useMutation, useQueryClient} from "react-query"; +import axios from "axios"; +import {AlbumImage} from "./model/AlbumImage"; + +export const useDeleteAlbumImage = () => { + const baseUrl = useBaseUrl(); + const queryClient = useQueryClient(); + return useMutation<void, unknown, AlbumImage>((image: AlbumImage) => axios.delete( + `api/v1/albums/${image.album}/images/${image.id}`, + { + baseURL: baseUrl + } + ), { + onSuccess: () => { + queryClient.invalidateQueries('album') + }, + }) +} diff --git a/ui/src/api/useDeleteImage.ts b/ui/src/api/useDeleteImage.ts index 46cb2517af0f9f196bd3cbd4d4f1797d566e302c..3b5ca932d06c8c903cf0851d9e12311efe7e276c 100644 --- a/ui/src/api/useDeleteImage.ts +++ b/ui/src/api/useDeleteImage.ts @@ -1,12 +1,12 @@ import {Image} from "./model/Image"; -import {useBaseUrl} from "./baseUrlContext"; +import {useBaseUrl} from "./util/BaseUrlContext"; import {useMutation, useQueryClient} from "react-query"; import axios from "axios"; export const useDeleteImage = () => { const baseUrl = useBaseUrl(); const queryClient = useQueryClient(); - return useMutation<unknown, unknown, Image>((image: Image) => axios.delete( + return useMutation<void, unknown, Image>((image: Image) => axios.delete( `api/v1/images/${image.id}`, { baseURL: baseUrl diff --git a/ui/src/api/useGetAlbum.ts b/ui/src/api/useGetAlbum.ts index 79c34a7bd3e383c612cf8945bcd0054a5bce3939..4e6481d752bc0ed9d212e0f845771d15c1331f55 100644 --- a/ui/src/api/useGetAlbum.ts +++ b/ui/src/api/useGetAlbum.ts @@ -1,11 +1,11 @@ import {useQuery} from "react-query"; import axios from "axios"; -import {useBaseUrl} from "./baseUrlContext"; +import {useBaseUrl} from "./util/BaseUrlContext"; import {Album} from "./model/Album"; export const useGetAlbum = (albumId: string) => { const baseUrl = useBaseUrl(); - return useQuery( + return useQuery<Album>( ["album", albumId], () => axios.get<Album>( `api/v1/albums/${albumId}`, diff --git a/ui/src/api/useGetImage.ts b/ui/src/api/useGetImage.ts index 6f6fc1b7e460d420f598b5b1788fb9613e339bf0..73488bdb8ef52b977e11d9d21bebf74e16d60f0b 100644 --- a/ui/src/api/useGetImage.ts +++ b/ui/src/api/useGetImage.ts @@ -1,11 +1,11 @@ import {useQuery} from "react-query"; import axios from "axios"; -import {useBaseUrl} from "./baseUrlContext"; +import {useBaseUrl} from "./util/BaseUrlContext"; import {Image} from "./model/Image"; export const useGetImage = (imageId: string) => { const baseUrl = useBaseUrl(); - return useQuery( + return useQuery<Image>( ["image", imageId], () => axios.get<Image>( `api/v1/images/${imageId}`, diff --git a/ui/src/api/useListAlbums.ts b/ui/src/api/useListAlbums.ts index 6e7b041be6ed0e26e63aedbcef07115759b8a057..fce4cd53c10c268c6a9ef0c067fea88363b6c233 100644 --- a/ui/src/api/useListAlbums.ts +++ b/ui/src/api/useListAlbums.ts @@ -1,11 +1,11 @@ import {useQuery} from "react-query"; import axios from "axios"; -import {useBaseUrl} from "./baseUrlContext"; +import {useBaseUrl} from "./util/BaseUrlContext"; import {Album} from "./model/Album"; export const useListAlbums = () => { const baseUrl = useBaseUrl(); - return useQuery( + return useQuery<Album[]>( "album", () => axios.get<Album[]>( "api/v1/albums", diff --git a/ui/src/api/useListImages.ts b/ui/src/api/useListImages.ts index 9d858b5635824b3b08b4d192909c94cc9beb39e0..e2bd33e4512776c8df9a256c1f1dc9a1c8ad1b35 100644 --- a/ui/src/api/useListImages.ts +++ b/ui/src/api/useListImages.ts @@ -1,11 +1,11 @@ import {useQuery} from "react-query"; import axios from "axios"; -import {useBaseUrl} from "./baseUrlContext"; +import {useBaseUrl} from "./util/BaseUrlContext"; import {Image} from "./model/Image"; export const useListImages = () => { const baseUrl = useBaseUrl(); - return useQuery( + return useQuery<Image[]>( "image", () => axios.get<Image[]>( "api/v1/images", diff --git a/ui/src/api/useReorderAlbum.ts b/ui/src/api/useReorderAlbum.ts new file mode 100644 index 0000000000000000000000000000000000000000..084af8385d247cb4a9dba8ef1b724ae1034431a8 --- /dev/null +++ b/ui/src/api/useReorderAlbum.ts @@ -0,0 +1,20 @@ +import {useBaseUrl} from "./util/BaseUrlContext"; +import {useMutation, useQueryClient} from "react-query"; +import axios from "axios"; +import {Album} from "./model/Album"; + +export const useReorderAlbum = () => { + const baseUrl = useBaseUrl(); + const queryClient = useQueryClient(); + return useMutation<void, unknown, Album>((album: Album) => axios.post( + `api/v1/albums/${album.id}/reorder`, + album.images, + { + baseURL: baseUrl + } + ), { + onSuccess: () => { + queryClient.invalidateQueries('album') + }, + }) +} diff --git a/ui/src/api/useUpdateAlbum.ts b/ui/src/api/useUpdateAlbum.ts new file mode 100644 index 0000000000000000000000000000000000000000..69e0f16717ca15241df5adb4e6e3ec1557a785e4 --- /dev/null +++ b/ui/src/api/useUpdateAlbum.ts @@ -0,0 +1,20 @@ +import {useBaseUrl} from "./util/BaseUrlContext"; +import {useMutation, useQueryClient} from "react-query"; +import axios from "axios"; +import {Album} from "./model/Album"; + +export const useUpdateAlbum = () => { + const baseUrl = useBaseUrl(); + const queryClient = useQueryClient(); + return useMutation<void, unknown, Album>((album: Album) => axios.post( + `api/v1/albums/${album.id}`, + album, + { + baseURL: baseUrl + } + ), { + onSuccess: () => { + queryClient.invalidateQueries('album') + }, + }) +} diff --git a/ui/src/api/useUpdateAlbumImage.ts b/ui/src/api/useUpdateAlbumImage.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb5a710fd13a62ca90e9689157cda3575ef92512 --- /dev/null +++ b/ui/src/api/useUpdateAlbumImage.ts @@ -0,0 +1,20 @@ +import {useBaseUrl} from "./util/BaseUrlContext"; +import {useMutation, useQueryClient} from "react-query"; +import axios from "axios"; +import {AlbumImage} from "./model/AlbumImage"; + +export const useUpdateAlbumImage = () => { + const baseUrl = useBaseUrl(); + const queryClient = useQueryClient(); + return useMutation<void, unknown, AlbumImage>((image: AlbumImage) => axios.post( + `api/v1/albums/${image.album}/images/${image.id}`, + image, + { + baseURL: baseUrl + } + ), { + onSuccess: () => { + queryClient.invalidateQueries('album') + }, + }) +} diff --git a/ui/src/api/useUpdateImage.ts b/ui/src/api/useUpdateImage.ts index 98dcffa34ec210a5396185dd1d094286b7b047b2..eef5991cd4e0bb98ff70677cd40dcc5f531429ed 100644 --- a/ui/src/api/useUpdateImage.ts +++ b/ui/src/api/useUpdateImage.ts @@ -1,7 +1,7 @@ -import {Image} from "./model/Image"; -import {useBaseUrl} from "./baseUrlContext"; +import {useBaseUrl} from "./util/BaseUrlContext"; import {useMutation, useQueryClient} from "react-query"; import axios from "axios"; +import {Image} from "./model/Image"; export const useUpdateImage = () => { const baseUrl = useBaseUrl(); diff --git a/ui/src/api/useUploadImage.ts b/ui/src/api/useUploadImage.ts index 925fc675363eaedee1551c0095fa9d18f521c135..d6d2035977ccf17c2fbe2b8576978a2f544595ae 100644 --- a/ui/src/api/useUploadImage.ts +++ b/ui/src/api/useUploadImage.ts @@ -1,5 +1,5 @@ import {Image} from "./model/Image"; -import {useBaseUrl} from "./baseUrlContext"; +import {useBaseUrl} from "./util/BaseUrlContext"; import {useMutation, useQueryClient} from "react-query"; import axios from "axios"; diff --git a/ui/src/api/baseUrlContext.ts b/ui/src/api/util/BaseUrlContext.ts similarity index 100% rename from ui/src/api/baseUrlContext.ts rename to ui/src/api/util/BaseUrlContext.ts diff --git a/ui/src/api/HttpMethod.ts b/ui/src/api/util/HttpMethod.ts similarity index 100% rename from ui/src/api/HttpMethod.ts rename to ui/src/api/util/HttpMethod.ts diff --git a/ui/src/components/AlbumCreateForm.tsx b/ui/src/components/AlbumCreateForm.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c33a9cab88e46ee68ecd3a4cbf716f74e19d19f7 --- /dev/null +++ b/ui/src/components/AlbumCreateForm.tsx @@ -0,0 +1,44 @@ +import {Fragment, useState} from "react"; +import {useCreateAlbum} from "../api/useCreateAlbum"; +import {LinearProgress} from "@material-ui/core"; +import {ErrorPortal} from "./error/ErrorContext"; +import {ErrorAlert} from "./error/ErrorAlert"; + +export function AlbumCreateForm() { + const {data: album, isLoading, error, mutate} = useCreateAlbum(); + const [title, setTitle] = useState<string>(""); + const [description, setDescription] = useState<string>(""); + return ( + <Fragment> + {isLoading && ( + <LinearProgress/> + )} + <ErrorPortal> + <ErrorAlert severity="error" error={error}/> + </ErrorPortal> + <form onSubmit={(event) => { + mutate({title, description}); + event.preventDefault(); + event.stopPropagation(); + }}> + <p>Create New Album</p> + <input + type="text" + name="title" + value={title} + onChange={({target: {value}}) => + setTitle(value)} + /> + <input + type="text" + name="description" + value={description} + onChange={({target: {value}}) => + setDescription(value)} + /> + <input type="submit"/> + <pre>{JSON.stringify(album, null, 2)}</pre> + </form> + </Fragment> + ); +} diff --git a/ui/src/components/AlbumImageView.tsx b/ui/src/components/AlbumImageView.tsx index 39b9c641e74c0cec4b7b796e7f45d91d04d7589b..9ffbc110c99e805fffadd5a90127c982fc2744ca 100644 --- a/ui/src/components/AlbumImageView.tsx +++ b/ui/src/components/AlbumImageView.tsx @@ -1,16 +1,64 @@ import {AlbumImage} from "../api/model/AlbumImage"; +import {useEffect, useState} from "react"; +import {Button, CircularProgress, TextField} from "@material-ui/core"; +import {Delete, Save} from "@material-ui/icons"; +import {useUpdateAlbumImage} from "../api/useUpdateAlbumImage"; +import {useDeleteAlbumImage} from "../api/useDeleteAlbumImage"; +import {ErrorPortal} from "./error/ErrorContext"; +import {ErrorAlert} from "./error/ErrorAlert"; -export interface AlbumImageProps { +export interface AlbumImageViewProps { image: AlbumImage } -export function AlbumImageView({image}: AlbumImageProps) { +export function AlbumImageView({image}: AlbumImageViewProps) { + const {mutate: update, error: updateError, isLoading: updateLoading} = useUpdateAlbumImage(); + const {mutate: remove, error: removeError, isLoading: removeLoading} = useDeleteAlbumImage(); + const [title, setTitle] = useState<string>(image?.title || ""); + const [description, setDescription] = useState<string>(image?.description || ""); + useEffect(() => setTitle(image?.title || ""), [image?.title]); + useEffect(() => setDescription(image?.description || ""), [image?.description]); + return ( - <div> - <p>{image.image}</p> - <p>{image.title}</p> - <p>{image.description}</p> - <img src={image.url + "t"} alt=""/> - </div> - ) + <li> + <ErrorPortal> + <ErrorAlert severity="error" error={updateError}/> + <ErrorAlert severity="error" error={removeError}/> + </ErrorPortal> + <form onSubmit={(event) => { + update({...image, title, description,}) + event.preventDefault(); + event.stopPropagation(); + }}> + <TextField + fullWidth + value={title} + onChange={({target: {value}}) => + setTitle(value)} + /> + <TextField + fullWidth + value={description} + onChange={({target: {value}}) => + setDescription(value)} + /> + <Button + variant="contained" + color="primary" + type="submit" + disabled={updateLoading} + startIcon={updateLoading ? <CircularProgress style={{color: "#fff"}} size="1em"/> : <Save/>} + >Save</Button> + <Button + variant="contained" + color="secondary" + disabled={removeLoading} + startIcon={removeLoading ? <CircularProgress style={{color: "#fff"}} size="1em"/> : <Delete/>} + onClick={() => remove(image)} + >Delete</Button> + </form> + <p>{image.url}</p> + <img src={image.url + "m"}/> + </li> + ); } diff --git a/ui/src/components/AlbumList.tsx b/ui/src/components/AlbumList.tsx deleted file mode 100644 index bedb720d82988ff41943be55414177b665377621..0000000000000000000000000000000000000000 --- a/ui/src/components/AlbumList.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import '../App.css'; -import {useListAlbums} from "../api/useListAlbums"; -import {AlbumView} from "./AlbumView"; - -export function AlbumList() { - const {status, data, error} = useListAlbums(); - return ( - <div> - <p>{status}</p> - <p>{error as string}</p> - <ul> - {data?.map(album => ( - <AlbumView - key={album.id} - album={album} - /> - ))} - </ul> - </div> - ); -} diff --git a/ui/src/components/AlbumView.tsx b/ui/src/components/AlbumView.tsx deleted file mode 100644 index 8b82de329467030a8abe2e5111ae3ecc9a2fe347..0000000000000000000000000000000000000000 --- a/ui/src/components/AlbumView.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import {Album} from "../api/model/Album"; -import {AlbumImageView} from "./AlbumImageView"; - -export interface AlbumProps { - album: Album -} - -export function AlbumView({album}: AlbumProps) { - return ( - <div> - <p>{album.id}</p> - <p>{album.owner}</p> - <p>{album.title}</p> - <p>{album.description}</p> - <p>{album.created_at}</p> - <p>{album.updated_at}</p> - <ul> - {album.images.map(image => ( - <AlbumImageView - key={image.image} - image={image} - /> - ))} - </ul> - </div> - ); -} diff --git a/ui/src/components/ImageListViewProps.tsx b/ui/src/components/ImageListViewProps.tsx deleted file mode 100644 index 0f7132b66d392c7a1cc753b3bb369f1b9f587d44..0000000000000000000000000000000000000000 --- a/ui/src/components/ImageListViewProps.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import {Fragment} from "react"; -import {Link} from "react-router-dom"; -import {Image} from "../api/model/Image"; -import {parseRatio} from "../metadata/Ratio"; - -export interface ImageListViewProps { - images: Image[], -} - -export function ImageListView({images}: ImageListViewProps) { - const rows: (Image | null)[][] = []; - if (images) { - let row = []; - for (let i = 0; i < images.length; i++) { - if (i % 4 === 0 && row.length !== 0) { - rows.push(row); - row = []; - } - row.push(images[i]); - } - rows.push([...row, ...([null, null, null].slice(0, 4 - row.length))]); - } - - return ( - <Fragment> - <style> - {`img { - height: 100%; - max-width: 100%; - margin: 2px; - } - - a:first-child img { - margin-left: -8px; - } - - a:last-child img { - margin-right: -8px; - }`} - </style> - <div style={{padding: "0 8px", fontSize: 0}}> - {rows.map(row => { - let ratios = 0; - for (const image of row) { - const [w, h] = image ? determineAspectRatio(image) : [1, 1]; - ratios += w / h; - } - - return ( - <div style={{aspectRatio: "" + ratios + "/" + 1}}> - {row.map(image => image && ( - <Link to={`/i/${image.id}`}> - <img src={image.url} alt=""/> - </Link> - ))} - </div> - ) - })} - </div> - </Fragment> - ) -} - -function determineAspectRatio(image: Image): [number, number] { - const aspectRatio = parseRatio(image.metadata["AspectRatio"]); - if (aspectRatio === undefined) { - console.log("Did not find aspect ratio: ", image.id); - return [1, 1]; - } else { - console.log("Found aspect ratio: ", image.id, aspectRatio.numerator, aspectRatio.denominator); - return [aspectRatio.numerator, aspectRatio.denominator]; - } -} - diff --git a/ui/src/components/UploadView.tsx b/ui/src/components/UploadView.tsx index 7d28e9c6c47a50eff8a8cdcc211cc973256b03c3..cce0a3a245b2230fecd37efa165ba64187f79fc4 100644 --- a/ui/src/components/UploadView.tsx +++ b/ui/src/components/UploadView.tsx @@ -1,6 +1,6 @@ import {useUploadImage} from "../api/useUploadImage"; -import {ErrorPortal} from "./ErrorContext"; -import {ErrorAlert} from "./ErrorAlert"; +import {ErrorPortal} from "./error/ErrorContext"; +import {ErrorAlert} from "./error/ErrorAlert"; import {LinearProgress} from "@material-ui/core"; export function UploadView() { diff --git a/ui/src/components/ErrorAlert.tsx b/ui/src/components/error/ErrorAlert.tsx similarity index 100% rename from ui/src/components/ErrorAlert.tsx rename to ui/src/components/error/ErrorAlert.tsx diff --git a/ui/src/components/ErrorContext.tsx b/ui/src/components/error/ErrorContext.tsx similarity index 100% rename from ui/src/components/ErrorContext.tsx rename to ui/src/components/error/ErrorContext.tsx diff --git a/ui/src/pages/AlbumDetailPage.tsx b/ui/src/pages/AlbumDetailPage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5c8046035ce52245cb16d16d7d2eb8b15429bfd2 --- /dev/null +++ b/ui/src/pages/AlbumDetailPage.tsx @@ -0,0 +1,151 @@ +import {Fragment, useEffect, useState} from "react"; +import {Button, CircularProgress, LinearProgress, TextField} from "@material-ui/core"; +import {Delete, Save} from "@material-ui/icons"; +import {useParams} from "react-router"; +import {ErrorPortal} from "../components/error/ErrorContext"; +import {ErrorAlert} from "../components/error/ErrorAlert"; +import {useGetAlbum} from "../api/useGetAlbum"; +import {useUpdateAlbum} from "../api/useUpdateAlbum"; +import {useDeleteAlbum} from "../api/useDeleteAlbum"; +import {useListImages} from "../api/useListImages"; +import {Image} from "../api/model/Image"; +import {useCreateAlbumImage} from "../api/useCreateAlbumImage"; +import {useReorderAlbum} from "../api/useReorderAlbum"; +import {AlbumImageView} from "../components/AlbumImageView"; + +export interface AlbumDetailPageParams { + albumId: string +} + +export function AlbumDetailPage() { + const {albumId} = useParams<AlbumDetailPageParams>(); + const {data: album, error: albumError, isLoading: albumLoading} = useGetAlbum(albumId); + const {mutate: update, error: updateError, isLoading: updateLoading} = useUpdateAlbum(); + const {mutate: remove, error: removeError, isLoading: removeLoading} = useDeleteAlbum(); + const [title, setTitle] = useState<string>(album?.title || ""); + const [description, setDescription] = useState<string>(album?.description || ""); + useEffect(() => setTitle(album?.title || ""), [album?.title]); + useEffect(() => setDescription(album?.description || ""), [album?.description]); + + const [addImage, setAddImage] = useState<string>(""); + const [imageTitle, setImageTitle] = useState<string>(""); + const [imageDescription, setImageDescription] = useState<string>(""); + + const {data: allImages, error: allImagesError, isLoading: allImagesLoading} = useListImages(); + + const {mutate: add, error: addImageError, isLoading: addImageLoading} = useCreateAlbumImage(); + const {mutate: reorder, error: reorderError, isLoading: reorderLoading} = useReorderAlbum(); + + if (album === undefined) { + return ( + <div>Error: 404</div> + ); + } + + return ( + <Fragment> + {albumLoading && ( + <LinearProgress/> + )} + <ErrorPortal> + <ErrorAlert severity="error" error={albumError}/> + <ErrorAlert severity="error" error={allImagesError}/> + <ErrorAlert severity="error" error={addImageError}/> + <ErrorAlert severity="error" error={updateError}/> + <ErrorAlert severity="error" error={removeError}/> + <ErrorAlert severity="error" error={reorderError}/> + </ErrorPortal> + <form onSubmit={(event) => { + update({...album, title, description,}) + event.preventDefault(); + event.stopPropagation(); + }}> + <TextField + fullWidth + value={title} + onChange={({target: {value}}) => + setTitle(value)} + /> + <TextField + fullWidth + value={description} + onChange={({target: {value}}) => + setDescription(value)} + /> + <Button + variant="contained" + color="primary" + type="submit" + disabled={updateLoading} + startIcon={updateLoading ? <CircularProgress style={{color: "#fff"}} size="1em"/> : <Save/>} + >Save</Button> + <Button + variant="contained" + color="secondary" + disabled={removeLoading} + startIcon={removeLoading ? <CircularProgress style={{color: "#fff"}} size="1em"/> : <Delete/>} + onClick={() => remove(album)} + >Delete</Button> + </form> + <form onSubmit={(event => { + add({ + album: albumId, + id: addImage, + title: imageTitle, + description: imageDescription + }); + event.preventDefault() + event.stopPropagation() + })}> + {allImagesLoading && ( + <CircularProgress/> + )} + <select + value={addImage} + onChange={({target: {value}}) => + setAddImage(value)} + > + <option value="">Select</option> + {allImages?.map((image: Image) => ( + <option value={image.id}>{image.title}</option> + ))} + </select> + <input + type="text" + value={imageTitle} + onChange={({target: {value}}) => + setImageTitle(value)} + /> + <input + type="text" + value={imageDescription} + onChange={({target: {value}}) => + setImageDescription(value)} + /> + <input type="submit"/> + {addImageLoading && ( + <CircularProgress/> + )} + </form> + <button onClick={() => { + const order = album?.images; + if (!order) { + console.log("Error: no images"); + } + reorder({ + ...album, + images: [...order].reverse() + }); + }}>Swap Order + </button> + {reorderLoading && ( + <CircularProgress/> + )} + <ul> + {album?.images?.map(image => ( + <AlbumImageView key={image.id} image={image}/> + ))} + </ul> + </Fragment> + ) +} diff --git a/ui/src/pages/AlbumListPage.tsx b/ui/src/pages/AlbumListPage.tsx index 5b675fc5d49fc3496e1fede92b1ec33f70fb5a8b..111d899ac5ea099d76b8f8d929da63fef26a6770 100644 --- a/ui/src/pages/AlbumListPage.tsx +++ b/ui/src/pages/AlbumListPage.tsx @@ -1,8 +1,9 @@ import {useListAlbums} from "../api/useListAlbums"; import {Fragment} from "react"; import {LinearProgress} from "@material-ui/core"; -import {ErrorPortal} from "../components/ErrorContext"; -import {ErrorAlert} from "../components/ErrorAlert"; +import {ErrorPortal} from "../components/error/ErrorContext"; +import {ErrorAlert} from "../components/error/ErrorAlert"; +import {AlbumCreateForm} from "../components/AlbumCreateForm"; export function AlbumListPage() { const {data: albums, error, isLoading} = useListAlbums(); @@ -15,6 +16,7 @@ export function AlbumListPage() { <ErrorPortal> <ErrorAlert severity="error" error={error}/> </ErrorPortal> + <AlbumCreateForm /> <ul> {albums?.map(album => ( <li> diff --git a/ui/src/pages/ImageDetailPage.tsx b/ui/src/pages/ImageDetailPage.tsx index c842e7fa6c753841737cbff5fcc6eba2b78c8da9..1d305dca98b9f60835a034bd4dedda49b2d90f2e 100644 --- a/ui/src/pages/ImageDetailPage.tsx +++ b/ui/src/pages/ImageDetailPage.tsx @@ -18,8 +18,8 @@ import {File, Tag} from "mdi-material-ui"; import {ImageMetadataView} from "../components/ImageMetadataView"; import {useGetImage} from "../api/useGetImage"; import {useParams} from "react-router"; -import {ErrorPortal} from "../components/ErrorContext"; -import {ErrorAlert} from "../components/ErrorAlert"; +import {ErrorPortal} from "../components/error/ErrorContext"; +import {ErrorAlert} from "../components/error/ErrorAlert"; export interface ImageDetailPageParams { imageId: string @@ -44,7 +44,11 @@ export function ImageDetailPage() { } return ( - <div> + <form onSubmit={(event) => { + update({...image, title, description,}) + event.preventDefault(); + event.stopPropagation(); + }}> {imageLoading && ( <LinearProgress/> )} @@ -97,13 +101,9 @@ export function ImageDetailPage() { <Button variant="contained" color="primary" + type="submit" disabled={updateLoading} startIcon={updateLoading ? <CircularProgress style={{color: "#fff"}} size="1em"/> : <Save/>} - onClick={() => update({ - ...image, - title, - description, - })} >Save</Button> <Button variant="contained" @@ -133,6 +133,6 @@ export function ImageDetailPage() { </List> </Grid> </Grid> - </div> + </form> ) } diff --git a/ui/src/pages/ImageListPage.tsx b/ui/src/pages/ImageListPage.tsx index e94f04c285eeb67edd6433d3fef43ddb5fc86c79..0e4abad71a94a888383303141da5f816aad238de 100644 --- a/ui/src/pages/ImageListPage.tsx +++ b/ui/src/pages/ImageListPage.tsx @@ -2,8 +2,8 @@ import {Fragment} from "react"; import {useListImages} from "../api/useListImages"; import {ImageList, ImageListItem, ImageListItemBar, LinearProgress} from "@material-ui/core"; import {Link} from "react-router-dom"; -import {ErrorPortal} from "../components/ErrorContext"; -import {ErrorAlert} from "../components/ErrorAlert"; +import {ErrorPortal} from "../components/error/ErrorContext"; +import {ErrorAlert} from "../components/error/ErrorAlert"; export function ImageListPage() { const {data: images, error, isLoading} = useListImages(); @@ -19,7 +19,7 @@ export function ImageListPage() { <ImageList cols={5}> {images?.map(image => ( <ImageListItem component={Link} to={`/i/${image.id}`}> - <img src={image.url} alt={image.title}/> + <img src={image.url+"m"} alt={image.title}/> <ImageListItemBar title={image.title} subtitle={image.original_name}