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}