From cceb60839aabbe03ffb6b5d0f30394dd384e3949 Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <janne@kuschku.de>
Date: Sun, 25 Jul 2021 16:14:55 +0200
Subject: [PATCH] Rewrite in progress

Signed-off-by: Janne Mareike Koschinski <janne@kuschku.de>
---
 assets/css/fonts.css                |   1 +
 assets/css/style.css                |   1 +
 assets/sass/_page_image_detail.sass |  63 +++++----
 go.mod                              |   2 +
 go.sum                              |   6 +
 main.go                             | 103 +++++++++++++--
 model/album.go                      |  22 ++++
 model/album_image.go                |   8 ++
 model/image.go                      |  25 ++++
 model/user.go                       |  17 +++
 page_album_detail.go                |  80 -----------
 page_album_list.go                  |  10 --
 page_image_detail.go                |   9 +-
 page_image_list.go                  |  49 -------
 page_index.go                       |   3 +-
 page_upload.go                      |  25 ++--
 repo/album_repository.go            | 198 ++++++++++++++++++++++++++++
 repo/image_repository.go            | 127 ++++++++++++++++++
 templates/_base.html                |   4 -
 templates/_footer.html              |   4 -
 templates/_header.html              |  25 ----
 templates/_navigation.html          |  20 ---
 templates/image_detail.html         | 138 +++++++++++--------
 templates/image_list.html           |  19 ---
 templates/index.html                |   2 -
 templates/upload.html               |  26 ----
 types.go                            |  36 +----
 util.go                             |  98 +++++++-------
 28 files changed, 693 insertions(+), 428 deletions(-)
 create mode 100644 assets/css/fonts.css
 create mode 100644 assets/css/style.css
 create mode 100644 model/album.go
 create mode 100644 model/album_image.go
 create mode 100644 model/image.go
 create mode 100644 model/user.go
 delete mode 100644 page_album_detail.go
 delete mode 100644 page_album_list.go
 delete mode 100644 page_image_list.go
 create mode 100644 repo/album_repository.go
 create mode 100644 repo/image_repository.go
 delete mode 100644 templates/_base.html
 delete mode 100644 templates/_footer.html
 delete mode 100644 templates/_header.html
 delete mode 100644 templates/_navigation.html
 delete mode 100644 templates/image_list.html
 delete mode 100644 templates/index.html
 delete mode 100644 templates/upload.html

diff --git a/assets/css/fonts.css b/assets/css/fonts.css
new file mode 100644
index 0000000..b2c0f8a
--- /dev/null
+++ b/assets/css/fonts.css
@@ -0,0 +1 @@
+@font-face{font-family:'Lato';src:url("/assets/fonts/Lato-Regular.eot");src:url("/assets/fonts/Lato-Regular.eot?#iefix") format("embedded-opentype"),url("/assets/fonts/Lato-Regular.woff2") format("woff2"),url("/assets/fonts/Lato-Regular.woff") format("woff"),url("/assets/fonts/Lato-Regular.ttf") format("truetype");font-style:normal;font-weight:normal;text-rendering:optimizeLegibility}@font-face{font-family:'Lato';src:url("/assets/fonts/Lato-Bold.eot");src:url("/assets/fonts/Lato-Bold.eot?#iefix") format("embedded-opentype"),url("/assets/fonts/Lato-Bold.woff2") format("woff2"),url("/assets/fonts/Lato-Bold.woff") format("woff"),url("/assets/fonts/Lato-Bold.ttf") format("truetype");font-style:normal;font-weight:bold;text-rendering:optimizeLegibility}@font-face{font-family:'Lato';src:url("/assets/fonts/Lato-BoldItalic.eot");src:url("/assets/fonts/Lato-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/assets/fonts/Lato-BoldItalic.woff2") format("woff2"),url("/assets/fonts/Lato-BoldItalic.woff") format("woff"),url("/assets/fonts/Lato-BoldItalic.ttf") format("truetype");font-style:italic;font-weight:bold;text-rendering:optimizeLegibility}@font-face{font-family:'Lato';src:url("/assets/fonts/Lato-Italic.eot");src:url("/assets/fonts/Lato-Italic.eot?#iefix") format("embedded-opentype"),url("/assets/fonts/Lato-Italic.woff2") format("woff2"),url("/assets/fonts/Lato-Italic.woff") format("woff"),url("/assets/fonts/Lato-Italic.ttf") format("truetype");font-style:italic;font-weight:normal;text-rendering:optimizeLegibility}
diff --git a/assets/css/style.css b/assets/css/style.css
new file mode 100644
index 0000000..df56c40
--- /dev/null
+++ b/assets/css/style.css
@@ -0,0 +1 @@
+*{margin:0;padding:0}*:focus{outline:none}*::-moz-focus-inner{border:0}html{height:100%}body{background:#282828;font-family:'Lato', sans-serif;font-size:81.25%;min-height:100%;display:flex;flex-direction:column}.container.centered{flex-grow:1;display:flex;margin:0 auto 64px auto;padding:0 16px;flex-direction:column;justify-content:center;width:100%;max-width:640px}nav{position:sticky;top:0;background:#333333;box-shadow:0 2px 4px rgba(0,0,0,0.2);padding:0 16px;z-index:100}nav ul{display:flex;max-width:1024px;margin:0 auto;height:56px;align-items:center}nav ul li,nav ul li>*{color:#282828;text-decoration:none}nav ul li{display:block;line-height:24px}@media (max-width: 640px){nav ul li.images,nav ul li.albums{display:none}}nav ul li.title a{display:inline-block;line-height:56px}nav ul li.title a img{width:32px;height:32px;vertical-align:middle}nav ul li.spacer{flex-grow:1}nav ul li:not(.spacer){margin:0 8px}nav ul li:not(.spacer):first-child{margin-left:0}nav ul li:not(.spacer):last-child{margin-right:0}nav ul li:not(.spacer):not(.title) a{display:block;background:#FFC107;padding:4px 16px;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1)}nav ul li:not(.spacer):not(.title) a:hover,nav ul li:not(.spacer):not(.title) a:focus{background:#FFD54F;box-shadow:0 2px 4px rgba(0,0,0,0.2)}.page.upload{flex-grow:1;display:flex;flex-direction:column}.page.upload .alert{padding:16px;margin:16px 0;box-shadow:0 2px 4px rgba(33,33,33,0.2);text-decoration:none;border-radius:2px}.page.upload .alert.success{background:#DCEDC8;color:#689F38;border-color:#689F38}.page.upload .alert.success a{color:#33691E}.page.upload .alert.error{background:#FFEBEE;color:#F44336;border-color:#F44336}.page.upload .alert.error a{color:#D32F2F}.page.upload form.upload{padding:96px 0;box-shadow:0 2px 4px rgba(0,0,0,0.2);text-decoration:none;border-radius:2px;text-align:center;background:#333333}.page.upload form.upload .upload-label{font-size:18pt;color:#fff}.page.upload form.upload label{position:relative;display:inline-block;overflow:hidden}.page.upload form.upload label span.text{position:relative;display:inline-block;background:#FFC107;padding:4px 16px;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1);line-height:24px;color:#282828;cursor:pointer;z-index:1}.page.upload form.upload label span.text:hover,.page.upload form.upload label span.text:focus{background:#FFD54F;box-shadow:0 2px 4px rgba(0,0,0,0.2)}.page.upload form.upload label input[type=file]{position:absolute;left:0;right:0;top:0;bottom:0;opacity:0}.page.upload .uploading-images{display:flex;flex-direction:row}.page.upload .uploading-images .images{display:flex;flex-direction:column;align-items:stretch;flex-grow:1}.page.upload .uploading-images .images .detail{margin-bottom:32px}.page.upload .uploading-images .images .detail .image{position:relative}.page.upload .uploading-images .images .detail .image .progress{position:absolute;top:-4px;left:0;right:0;height:4px;display:block;background-color:rgba(255,193,7,0.2);overflow:hidden;transition:opacity 400ms}.page.upload .uploading-images .images .detail .image .progress .indeterminate{background-color:rgba(255,193,7,0.8)}.page.upload .uploading-images .images .detail .image .progress .indeterminate::before{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite}.page.upload .uploading-images .images .detail .image .progress .indeterminate::after{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;animation-delay:1.125s}.page.upload .uploading-images .images .detail:not(.uploading) .progress{opacity:0}.page.upload.submitted .container.centered{display:none}.page.upload:not(.submitted) .uploading-images{display:none}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}.page.image.detail,.page.upload .uploading-images{display:flex;flex-direction:row;align-items:start;align-self:center;padding:32px 32px 128px;justify-content:center}@media (max-width: 1024px){.page.image.detail,.page.upload .uploading-images{flex-direction:column;padding:32px 0;align-items:stretch;justify-content:start}}.page.image.detail .sidebar,.page.upload .uploading-images .sidebar{width:250px;max-width:640px;margin-left:32px}.page.image.detail .sidebar .info,.page.upload .uploading-images .sidebar .info{background:#333333;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,0.4);padding:8px;margin-bottom:8px;color:#ffffff}@media (max-width: 1024px){.page.image.detail .sidebar .info,.page.upload .uploading-images .sidebar .info{padding:16px;margin-bottom:32px}}.page.image.detail .sidebar .url div,.page.upload .uploading-images .sidebar .url div{display:flex;flex-direction:row}.page.image.detail .sidebar .url div input,.page.upload .uploading-images .sidebar .url div input{background:none;color:#fff;opacity:0.4;border:none;flex-shrink:1;display:block;flex-grow:1;width:0;text-overflow:ellipsis}.page.image.detail .sidebar .url div button.copy,.page.upload .uploading-images .sidebar .url div button.copy{display:block;background:#FFC107;padding:4px 16px;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1);border:none;font-family:'Lato', sans-serif;font-size:10pt;line-height:24px;margin-left:8px;cursor:pointer}.page.image.detail .sidebar .url div button.copy:hover,.page.image.detail .sidebar .url div button.copy:focus,.page.upload .uploading-images .sidebar .url div button.copy:hover,.page.upload .uploading-images .sidebar .url div button.copy:focus{background:#FFD54F;box-shadow:0 2px 4px rgba(0,0,0,0.2)}.page.image.detail .sidebar .actions,.page.upload .uploading-images .sidebar .actions{background:#333333;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,0.4);display:flex;flex-direction:row;margin-bottom:8px}@media (max-width: 1024px){.page.image.detail .sidebar .actions,.page.upload .uploading-images .sidebar .actions{margin-bottom:32px}}.page.image.detail .sidebar .actions .delete-form,.page.image.detail .sidebar .actions .update-form,.page.upload .uploading-images .sidebar .actions .delete-form,.page.upload .uploading-images .sidebar .actions .update-form{display:flex;flex-grow:1;flex-basis:0}.page.image.detail .sidebar .actions .delete-form input[type=submit],.page.image.detail .sidebar .actions .update-form input[type=submit],.page.upload .uploading-images .sidebar .actions .delete-form input[type=submit],.page.upload .uploading-images .sidebar .actions .update-form input[type=submit]{display:block;background:#FFC107;padding:4px 16px;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1);border:none;font-family:'Lato', sans-serif;font-size:10pt;line-height:24px;margin:8px;flex-grow:1;cursor:pointer}@media (max-width: 1024px){.page.image.detail .sidebar .actions .delete-form input[type=submit],.page.image.detail .sidebar .actions .update-form input[type=submit],.page.upload .uploading-images .sidebar .actions .delete-form input[type=submit],.page.upload .uploading-images .sidebar .actions .update-form input[type=submit]{margin:16px}}.page.image.detail .sidebar .actions .delete-form input[type=submit]:hover,.page.image.detail .sidebar .actions .delete-form input[type=submit]:focus,.page.image.detail .sidebar .actions .update-form input[type=submit]:hover,.page.image.detail .sidebar .actions .update-form input[type=submit]:focus,.page.upload .uploading-images .sidebar .actions .delete-form input[type=submit]:hover,.page.upload .uploading-images .sidebar .actions .delete-form input[type=submit]:focus,.page.upload .uploading-images .sidebar .actions .update-form input[type=submit]:hover,.page.upload .uploading-images .sidebar .actions .update-form input[type=submit]:focus{background:#FFD54F;box-shadow:0 2px 4px rgba(0,0,0,0.2)}@media (max-width: 1024px){.page.image.detail .sidebar,.page.upload .uploading-images .sidebar{width:auto;margin-left:0;margin-top:32px}}.page.image.detail .detail,.page.upload .uploading-images .detail{max-width:640px;width:100%;background:#333333;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,0.4)}.page.image.detail .detail .title,.page.upload .uploading-images .detail .title{line-height:1.25;padding:16px;vertical-align:middle;font-size:14pt;font-weight:normal;color:#eee;background:none;border:none;width:100%;box-sizing:border-box}.page.image.detail .detail .title:not(.fake-input):empty,.page.upload .uploading-images .detail .title:not(.fake-input):empty{display:none}.page.image.detail .detail .title:not(.fake-input):empty+.image,.page.upload .uploading-images .detail .title:not(.fake-input):empty+.image{border-top-left-radius:4px;border-top-right-radius:4px}.page.image.detail .detail .image,.page.upload .uploading-images .detail .image{background:#000;display:flex;flex-direction:row;justify-content:center;align-items:start}.page.image.detail .detail .image img,.page.upload .uploading-images .detail .image img{max-width:100%}.page.image.detail .detail .description,.page.upload .uploading-images .detail .description{line-height:1.25;padding:16px;vertical-align:middle;font-size:11pt;font-weight:normal;color:#eee;background:none;border:none;width:100%;box-sizing:border-box;font-family:'Lato', sans-serif;resize:vertical;white-space:pre-line}.page.image.detail .detail .description:not(.fake-input):empty,.page.upload .uploading-images .detail .description:not(.fake-input):empty{display:none}.page.image.detail .fake-input[contenteditable]:empty:before,.page.upload .uploading-images .fake-input[contenteditable]:empty:before{opacity:0.4;content:attr(placeholder)}.page.image.list{display:flex;max-width:1024px;margin:0 auto;align-items:start;flex-wrap:wrap}.page.image.list .image{padding:8px;margin:8px;position:relative;box-shadow:0 2px 4px rgba(0,0,0,0.2);transition:all 200ms;text-decoration:none;width:160px;background:#333333;border-radius:2px}.page.image.list .image:hover,.page.image.list .image:focus{margin-top:4px;box-shadow:0 4px 6px rgba(0,0,0,0.4)}.page.image.list .image .image-container{display:flex;justify-content:center;align-content:center;justify-items:center;align-items:center;flex-direction:column;height:160px;width:160px;background:#000000}.page.image.list .image .info{display:block;z-index:1;color:#eeeeee;line-height:1.25;font-size:10pt;padding-top:12px}.page.image.list .image .info p{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
diff --git a/assets/sass/_page_image_detail.sass b/assets/sass/_page_image_detail.sass
index e1bccc7..3a2f06f 100644
--- a/assets/sass/_page_image_detail.sass
+++ b/assets/sass/_page_image_detail.sass
@@ -14,45 +14,44 @@
     width: 250px
     max-width: 640px
     margin-left: 32px
-    .url
+    .info
       background: #333333
       border-radius: 4px
       box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4)
       padding: 8px
       margin-bottom: 8px
+      color: #ffffff
       @media (max-width: 1024px)
         padding: 16px
         margin-bottom: 32px
-      p
-        color: #ffffff
-      div
-        display: flex
-        flex-direction: row
-        input
-          background: none
-          color: #fff
-          opacity: 0.4
-          border: none
-          flex-shrink: 1
-          display: block
-          flex-grow: 1
-          width: 0
-          text-overflow: ellipsis
-        button.copy
-          display: block
-          background: #FFC107
-          padding: 4px 16px
-          border-radius: 2px
-          box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)
-          border: none
-          font-family: 'Lato', sans-serif
-          font-size: 10pt
-          line-height: 24px
-          margin-left: 8px
-          cursor: pointer
-          &:hover, &:focus
-            background: #FFD54F
-            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2)
+    .url div
+      display: flex
+      flex-direction: row
+      input
+        background: none
+        color: #fff
+        opacity: 0.4
+        border: none
+        flex-shrink: 1
+        display: block
+        flex-grow: 1
+        width: 0
+        text-overflow: ellipsis
+      button.copy
+        display: block
+        background: #FFC107
+        padding: 4px 16px
+        border-radius: 2px
+        box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)
+        border: none
+        font-family: 'Lato', sans-serif
+        font-size: 10pt
+        line-height: 24px
+        margin-left: 8px
+        cursor: pointer
+        &:hover, &:focus
+          background: #FFD54F
+          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2)
     .actions
       background: #333333
       border-radius: 4px
@@ -136,4 +135,4 @@
         display: none
   .fake-input[contenteditable]:empty:before
     opacity: 0.4
-    content: attr(placeholder)
\ No newline at end of file
+    content: attr(placeholder)
diff --git a/go.mod b/go.mod
index 302c4aa..9780623 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,8 @@ go 1.13
 
 require (
 	github.com/go-redis/redis v6.15.6+incompatible
+	github.com/gorilla/mux v1.8.0 // indirect
+	github.com/justjanne/imgconv v1.0.3
 	github.com/lib/pq v1.10.2
 	github.com/onsi/ginkgo v1.10.3 // indirect
 	github.com/onsi/gomega v1.7.1 // indirect
diff --git a/go.sum b/go.sum
index cc0e436..be2231f 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,12 @@ github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be
 github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
 github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/justjanne/imgconv v1.0.3 h1:XXgqeLJ1ibV0XCdosMqVV65CdpmDADTnC3yBR2w52vE=
+github.com/justjanne/imgconv v1.0.3/go.mod h1:8VxkQjMdEOziT9LJ8Mrqtz7SbmqlpExl69K8/CZWeAc=
 github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
 github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -25,6 +29,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gographics/imagick.v2 v2.6.0 h1:ewRsUQk3QkjGumERlndbFn/kTYRjyMaPY5gxwpuAhik=
+gopkg.in/gographics/imagick.v2 v2.6.0/go.mod h1:/QVPLV/iKdNttRKthmDkeeGg+vdHurVEPc8zkU0XgBk=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
diff --git a/main.go b/main.go
index 4976acf..f3d0649 100644
--- a/main.go
+++ b/main.go
@@ -2,7 +2,10 @@ package main
 
 import (
 	"database/sql"
+	"fmt"
+	"git.kuschku.de/justjanne/imghost-frontend/repo"
 	"github.com/go-redis/redis"
+	"github.com/gorilla/mux"
 	_ "github.com/lib/pq"
 	"net/http"
 )
@@ -26,20 +29,102 @@ func main() {
 		http.FileServer(http.Dir("assets")),
 	}
 
-	http.Handle("/upload/", pageUpload(pageContext))
+	imageRepo := repo.NewImageRepository(pageContext.Database)
+	albumRepo := repo.NewAlbumRepository(pageContext.Database)
 
-	http.Handle("/i/", http.StripPrefix("/i/", pageImageDetail(pageContext)))
-	http.Handle("/a/", http.StripPrefix("/a/", pageAlbumDetail(pageContext)))
+	router := mux.NewRouter().StrictSlash(true)
+	router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte("Home Page"))
+	})
+	router.HandleFunc("/i/{imageId}", func(w http.ResponseWriter, r *http.Request) {
+		var err error
+
+		user := parseUser(r)
+		imageId := mux.Vars(r)["imageId"]
+		image, err := imageRepo.Get(imageId)
+		if err != nil { panic(err) }
+
+		err = formatTemplate(w, "image_detail.html", ImageDetailData{
+			user,
+			image,
+		})
+		if err != nil { panic(err) }
+	}).Methods("GET")
+	router.HandleFunc("/i/{imageId}", func(w http.ResponseWriter, r *http.Request) {
+		var err error
+
+		user := parseUser(r)
+		imageId := mux.Vars(r)["imageId"]
+		image, err := imageRepo.Get(imageId)
+		if err != nil { panic(err) }
+
+		err = image.VerifyOwner(user)
+		if err != nil { panic(err) }
+
+		image.Title = r.FormValue("title")
+		image.Description = r.FormValue("description")
+
+		err = imageRepo.Update(image)
+		if err != nil { panic(err) }
+
+		http.Redirect(w, r, "/i/" + imageId, http.StatusFound)
+	}).Methods("POST")
+	router.HandleFunc("/a/{albumId}", func(w http.ResponseWriter, r *http.Request) {
+		var err error
+
+		user := parseUser(r)
+		albumId := mux.Vars(r)["albumId"]
+		album, err := albumRepo.Get(albumId)
+		if err != nil { panic(err) }
+		images, err := albumRepo.GetImages(album)
+		if err != nil { panic(err) }
+
+		err = formatTemplate(w, "album_detail.html", AlbumDetailData{
+			user,
+			album,
+			images,
+		})
+		if err != nil { panic(err) }
+	}).Methods("GET")
+	router.HandleFunc("/a/{albumId}", func(w http.ResponseWriter, r *http.Request) {
+		var err error
+
+		user := parseUser(r)
+		albumId := mux.Vars(r)["albumId"]
+		album, err := albumRepo.Get(albumId)
+		if err != nil { panic(err) }
+
+		err = album.VerifyOwner(user)
+		if err != nil { panic(err) }
+
+		album.Title = r.FormValue("title")
+		album.Description = r.FormValue("description")
+
+		err = albumRepo.Update(album)
+		if err != nil { panic(err) }
+
+		http.Redirect(w, r, "/a/" + albumId, http.StatusFound)
+	}).Methods("POST")
+	router.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(fmt.Sprintf("postgres: %v\n", pageContext.Database.Ping())))
+		_, _ = w.Write([]byte(fmt.Sprintf("redis: %v\n", pageContext.Redis.Ping().Err())))
+	})
+	router.HandleFunc("/{imageId}", func(w http.ResponseWriter, r *http.Request) {
+		var err error
 
-	http.Handle("/me/images/", pageImageList(pageContext))
-	http.Handle("/assets/", http.StripPrefix("/assets/", pageContext.AssetServer))
-	http.Handle("/", pageIndex(pageContext))
+		user := parseUser(r)
+		imageId := mux.Vars(r)["imageId"]
+		image, err := imageRepo.Get(imageId)
+		if err != nil { panic(err) }
 
-	http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
-		w.Write([]byte("OK"))
+		err = formatTemplate(w, "image_detail.html", ImageDetailData{
+			user,
+			image,
+		})
+		if err != nil { panic(err) }
 	})
 
-	err = http.ListenAndServe(":8080", nil)
+	err = http.ListenAndServe(":8080", router)
 	if err != nil {
 		panic(err)
 	}
diff --git a/model/album.go b/model/album.go
new file mode 100644
index 0000000..c385d91
--- /dev/null
+++ b/model/album.go
@@ -0,0 +1,22 @@
+package model
+
+import (
+	"errors"
+	"time"
+)
+
+type Album struct {
+	Id          string    `json:"id"`
+	Owner       string    `json:"owner"`
+	Title       string    `json:"title"`
+	Description string    `json:"description"`
+	CreatedAt   time.Time `json:"created_at"`
+}
+
+func (album Album) VerifyOwner(user User) error {
+	if album.Owner != user.Id {
+		return errors.New("user does not have ownership over this item")
+	}
+
+	return nil
+}
diff --git a/model/album_image.go b/model/album_image.go
new file mode 100644
index 0000000..3379dfe
--- /dev/null
+++ b/model/album_image.go
@@ -0,0 +1,8 @@
+package model
+
+type AlbumImage struct {
+	Id          string `json:"id"`
+	Title       string `json:"title"`
+	Description string `json:"description"`
+	Position    int    `json:"position"`
+}
diff --git a/model/image.go b/model/image.go
new file mode 100644
index 0000000..695ad83
--- /dev/null
+++ b/model/image.go
@@ -0,0 +1,25 @@
+package model
+
+import (
+	"errors"
+	"time"
+)
+
+type Image struct {
+	Id           string    `json:"id"`
+	Owner        string    `json:"owner"`
+	Title        string    `json:"title"`
+	Description  string    `json:"description"`
+	CreatedAt    time.Time `json:"created_at"`
+	UpdatedAt    time.Time `json:"updated_at"`
+	OriginalName string    `json:"original_name"`
+	MimeType     string    `json:"mime_type"`
+}
+
+func (image Image) VerifyOwner(user User) error {
+	if image.Owner != user.Id {
+		return errors.New("user does not have ownership over this item")
+	}
+
+	return nil
+}
diff --git a/model/user.go b/model/user.go
new file mode 100644
index 0000000..84c1bfd
--- /dev/null
+++ b/model/user.go
@@ -0,0 +1,17 @@
+package model
+
+type User struct {
+	Id    string   `json:"id"`
+	Name  string   `json:"name"`
+	Email string   `json:"email"`
+	Roles []string `json:"roles"`
+}
+
+func (info User) HasRole(role string) bool {
+	for _, r := range info.Roles {
+		if r == role {
+			return true
+		}
+	}
+	return false
+}
diff --git a/page_album_detail.go b/page_album_detail.go
deleted file mode 100644
index 440423f..0000000
--- a/page_album_detail.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package main
-
-import (
-	"net/http"
-	"fmt"
-	"path"
-)
-
-type AlbumDetailData struct {
-	User   UserInfo
-	Album  Album
-	IsMine bool
-}
-
-func pageAlbumDetail(ctx PageContext) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		user := parseUser(r)
-
-		_, albumId := path.Split(r.URL.Path)
-		result, err := ctx.Database.Query(`
-			SELECT
-				id,
-				owner,
-				coalesce(title,  ''),
-				coalesce(description, ''),
-        		coalesce(created_at, to_timestamp(0))
-			FROM albums
-			WHERE id = $1
-			`, albumId)
-		if err != nil {
-			panic(err)
-		}
-
-		var info Album
-		if result.Next() {
-			var owner string
-			err := result.Scan(&info.Id, &owner, &info.Title, &info.Description, &info.CreatedAt)
-			if err != nil {
-				panic(err)
-			}
-
-			result, err := ctx.Database.Query(`
-			SELECT
-				image,
-				title,
-				description,
-				position
-			FROM album_images
-			WHERE album = $1
-			ORDER BY position ASC
-			`, albumId)
-			if err != nil {
-				panic(err)
-			}
-
-			for result.Next() {
-				var image AlbumImage
-				err := result.Scan(&image.Id, &owner, &image.Title, &image.Description, &image.Position)
-				if err != nil {
-					panic(err)
-				}
-
-				info.Images = append(info.Images, image)
-			}
-
-			if err = formatTemplate(w, "album_detail.html", AlbumDetailData{
-				user,
-				info,
-				owner == user.Id,
-			}); err != nil {
-				panic(err)
-			}
-
-			return
-		}
-
-		w.WriteHeader(http.StatusNotFound)
-		fmt.Fprint(w, "Album not found")
-	})
-}
diff --git a/page_album_list.go b/page_album_list.go
deleted file mode 100644
index 62d66dd..0000000
--- a/page_album_list.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package main
-
-import (
-	"net/http"
-)
-
-func pageAlbumList(ctx PageContext) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-	})
-}
diff --git a/page_image_detail.go b/page_image_detail.go
index 2cc76cf..8759da5 100644
--- a/page_image_detail.go
+++ b/page_image_detail.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"fmt"
+	"git.kuschku.de/justjanne/imghost-frontend/model"
 	_ "github.com/lib/pq"
 	"net/http"
 	"os"
@@ -9,9 +10,8 @@ import (
 )
 
 type ImageDetailData struct {
-	User   UserInfo
-	Image  Image
-	IsMine bool
+	User   model.User
+	Image  model.Image
 }
 
 func pageImageDetail(ctx PageContext) http.Handler {
@@ -37,7 +37,7 @@ func pageImageDetail(ctx PageContext) http.Handler {
 			return
 		}
 
-		var info Image
+		var info model.Image
 
 		if result.Next() {
 			var owner string
@@ -90,7 +90,6 @@ func pageImageDetail(ctx PageContext) http.Handler {
 			if err = formatTemplate(w, "image_detail.html", ImageDetailData{
 				user,
 				info,
-				owner == user.Id,
 			}); err != nil {
 				panic(err)
 			}
diff --git a/page_image_list.go b/page_image_list.go
deleted file mode 100644
index fb1950d..0000000
--- a/page_image_list.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package main
-
-import (
-	"net/http"
-)
-
-type ImageListData struct {
-	User   UserInfo
-	Images []Image
-}
-
-func pageImageList(ctx PageContext) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		user := parseUser(r)
-
-		result, err := ctx.Database.Query(`
-			SELECT
-				id,
-				coalesce(title,  ''),
-				coalesce(description, ''),
-        		coalesce(created_at, to_timestamp(0)),
-				coalesce(original_name, ''),
-				coalesce(type, '')
-			FROM images
-			WHERE owner = $1
-			ORDER BY created_at DESC
-			`, user.Id)
-		if err != nil {
-			panic(err)
-		}
-
-		var images []Image
-		for result.Next() {
-			var info Image
-			err := result.Scan(&info.Id, &info.Title, &info.Description, &info.CreatedAt, &info.OriginalName, &info.MimeType)
-			if err != nil {
-				panic(err)
-			}
-			images = append(images, info)
-		}
-
-		if err = formatTemplate(w, "image_list.html", ImageListData{
-			user,
-			images,
-		}); err != nil {
-			panic(err)
-		}
-	})
-}
diff --git a/page_index.go b/page_index.go
index a2052fd..f1decb4 100644
--- a/page_index.go
+++ b/page_index.go
@@ -1,12 +1,13 @@
 package main
 
 import (
+	"git.kuschku.de/justjanne/imghost-frontend/model"
 	"net/http"
 	"strings"
 )
 
 type IndexData struct {
-	User UserInfo
+	User model.User
 }
 
 func removeFileExtensions(path string) string {
diff --git a/page_upload.go b/page_upload.go
index 23154e6..7a5cf16 100644
--- a/page_upload.go
+++ b/page_upload.go
@@ -1,20 +1,21 @@
 package main
 
 import (
-	"net/http"
-	"fmt"
-	"time"
+	"crypto/rand"
+	"encoding/base64"
 	"encoding/json"
+	"fmt"
+	"git.kuschku.de/justjanne/imghost-frontend/model"
 	"io"
 	"mime/multipart"
-	"path/filepath"
+	"net/http"
 	"os"
-	"encoding/base64"
-	"crypto/rand"
+	"path/filepath"
+	"time"
 )
 
 type UploadData struct {
-	User    UserInfo
+	User    model.User
 	Results []Result
 }
 
@@ -54,21 +55,21 @@ func writeBody(reader io.ReadCloser, path string) error {
 	return out.Close()
 }
 
-func createImage(config *Config, body io.ReadCloser, fileHeader *multipart.FileHeader) (Image, error) {
+func createImage(config *Config, body io.ReadCloser, fileHeader *multipart.FileHeader) (model.Image, error) {
 	id := generateId()
 	path := filepath.Join(config.SourceFolder, id)
 
 	err := writeBody(body, path)
 	if err != nil {
-		return Image{}, err
+		return model.Image{}, err
 	}
 
 	mimeType, err := detectMimeType(path)
 	if err != nil {
-		return Image{}, err
+		return model.Image{}, err
 	}
 
-	image := Image{
+	image := model.Image{
 		Id:           id,
 		OriginalName: filepath.Base(fileHeader.Filename),
 		CreatedAt:    time.Now(),
@@ -77,7 +78,7 @@ func createImage(config *Config, body io.ReadCloser, fileHeader *multipart.FileH
 	return image, nil
 }
 
-func pageUpload(ctx PageContext) http.Handler {
+func pageUpload(ctx PageContext) http.HandlerFunc {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		if r.Method == "POST" {
 			user := parseUser(r)
diff --git a/repo/album_repository.go b/repo/album_repository.go
new file mode 100644
index 0000000..4ced2fb
--- /dev/null
+++ b/repo/album_repository.go
@@ -0,0 +1,198 @@
+package repo
+
+import (
+	"database/sql"
+	"git.kuschku.de/justjanne/imghost-frontend/model"
+	"time"
+)
+
+type AlbumRepository struct {
+	db *sql.DB
+}
+
+func NewAlbumRepository(db *sql.DB) AlbumRepository {
+	return AlbumRepository{
+		db: db,
+	}
+}
+
+func (repo AlbumRepository) List(user model.User) ([]model.Album, error) {
+	var albums []model.Album
+
+	result, err := repo.db.Query(`
+			SELECT
+				id,
+			    owner,
+				coalesce(title,  ''),
+				coalesce(description, ''),
+        		coalesce(created_at, to_timestamp(0)),
+				coalesce(original_name, ''),
+				coalesce(type, '')
+			FROM images
+			WHERE owner = $1
+			ORDER BY created_at DESC
+			`, user.Id)
+	if err != nil {
+		return albums, err
+	}
+
+	for result.Next() {
+		var album model.Album
+
+		if err := result.Scan(
+			&album.Id, &album.Owner, &album.Title, &album.Description,
+			&album.CreatedAt,
+		); err != nil {
+			return albums, err
+		}
+		albums = append(albums, album)
+	}
+
+	return albums, nil
+}
+
+func (repo AlbumRepository) Get(albumId string) (model.Album, error) {
+	var album model.Album
+
+	result, err := repo.db.Query(`
+			SELECT
+				id,
+				owner,
+				coalesce(title,  ''),
+				coalesce(description, ''),
+        		coalesce(created_at, to_timestamp(0))
+			FROM albums
+			WHERE id = $1`,
+			albumId)
+	if err != nil {
+		return album, err
+	}
+
+	if result.Next() {
+		if err := result.Scan(
+			&album.Id, &album.Owner, &album.Title, &album.Description,
+			&album.CreatedAt,
+		); err != nil {
+			return album, err
+		}
+	}
+
+	return album, nil
+}
+
+func (repo AlbumRepository) Create(album model.Album) error {
+	if _, err := repo.db.Exec(`
+		INSERT INTO albums (id, owner, title, description, created_at, updated_at) 
+		VALUES ($1, $2, $3, $4, $5)`,
+		album.Id,
+		album.Owner,
+		album.Title,
+		album.Description,
+		time.Now().UTC(),
+		time.Now().UTC(),
+	); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (repo AlbumRepository) Update(album model.Album) error {
+	if _, err := repo.db.Exec(
+		"UPDATE albums SET title = $1, description = $2, updated_at = $3 WHERE id = $4",
+		album.Title,
+		album.Description,
+		time.Now().UTC(),
+		album.Id,
+	); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (repo AlbumRepository) Delete(album model.Album) error {
+	if _, err := repo.db.Exec(
+		"DELETE FROM albums WHERE id = $1",
+		album.Id,
+	); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (repo AlbumRepository) AddImage(album model.Album, image model.AlbumImage) error {
+	if _, err := repo.db.Exec(`
+		INSERT INTO album_images (album, image, title, description, position) 
+		VALUES ($1, $2, $3, $4)`,
+		album.Id,
+		image.Id,
+		image.Title,
+		image.Description,
+		image.Position,
+	); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (repo AlbumRepository) GetImages(album model.Album) ([]model.AlbumImage, error) {
+	var albumImages []model.AlbumImage
+
+	result, err := repo.db.Query(`
+			SELECT
+				image,
+				coalesce(title,  ''),
+				coalesce(description, ''),
+			    position
+			FROM album_images`)
+	if err != nil {
+		return albumImages, err
+	}
+
+	for result.Next() {
+		var albumImage model.AlbumImage
+
+		if err := result.Scan(
+			&albumImage.Id, &albumImage.Title, &albumImage.Description,
+			&albumImage.Position,
+		); err != nil {
+			return albumImages, err
+		}
+		albumImages = append(albumImages, albumImage)
+	}
+
+	return albumImages, nil
+}
+
+func (repo AlbumRepository) RemoveImage(album model.Album, imageId string) error {
+	if _, err := repo.db.Exec(
+		"DELETE FROM album_images WHERE album = $1 AND image = $2",
+		album.Id,
+		imageId,
+	); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (repo AlbumRepository) ReorderImages(album model.Album, images []model.AlbumImage) error {
+	if _, err := repo.db.Exec(
+		"DELETE FROM album_images WHERE album = $1",
+		album.Id,
+	); err != nil {
+		return err
+	}
+
+	for _, image := range images {
+		if err := repo.AddImage(album, image); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
diff --git a/repo/image_repository.go b/repo/image_repository.go
new file mode 100644
index 0000000..83b8821
--- /dev/null
+++ b/repo/image_repository.go
@@ -0,0 +1,127 @@
+package repo
+
+import (
+	"database/sql"
+	"git.kuschku.de/justjanne/imghost-frontend/model"
+	"time"
+)
+
+type ImageRepository struct {
+	db *sql.DB
+}
+
+func NewImageRepository(db *sql.DB) ImageRepository {
+	return ImageRepository{
+		db: db,
+	}
+}
+
+func (repo ImageRepository) List(user model.User) ([]model.Image, error) {
+	var images []model.Image
+
+	result, err := repo.db.Query(`
+			SELECT
+				id,
+			    owner,
+				coalesce(title,  ''),
+				coalesce(description, ''),
+        		coalesce(created_at, to_timestamp(0)),
+				coalesce(original_name, ''),
+				coalesce(type, '')
+			FROM images
+			WHERE owner = $1
+			ORDER BY created_at DESC
+			`, user.Id)
+	if err != nil {
+		return images, err
+	}
+
+	for result.Next() {
+		var image model.Image
+
+		if err := result.Scan(
+			&image.Id, &image.Owner, &image.Title, &image.Description,
+			&image.CreatedAt, &image.OriginalName, &image.MimeType,
+		); err != nil {
+			return images, err
+		}
+		images = append(images, image)
+	}
+
+	return images, nil
+}
+
+func (repo ImageRepository) Get(imageId string) (model.Image, error) {
+	var image model.Image
+
+	result, err := repo.db.Query(`
+			SELECT
+				id,
+				owner,
+				coalesce(title,  ''),
+				coalesce(description, ''),
+        		coalesce(created_at, to_timestamp(0)),
+				coalesce(original_name, ''),
+				coalesce(type, '')
+			FROM images
+			WHERE id = $1
+			`, imageId)
+	if err != nil {
+		return image, err
+	}
+
+	if result.Next() {
+		if err := result.Scan(
+			&image.Id, &image.Owner, &image.Title, &image.Description,
+			&image.CreatedAt, &image.OriginalName, &image.MimeType,
+		); err != nil {
+			return image, err
+		}
+	}
+
+	return image, nil
+}
+
+func (repo ImageRepository) Create(image model.Image) error {
+	if _, err := repo.db.Exec(`
+		INSERT INTO images (id, owner, title, description, created_at, updated_at, original_name, type) 
+		VALUES ($1, $2, $3, $4, $5)`,
+		image.Id,
+		image.Owner,
+		image.Title,
+		image.Description,
+		time.Now().UTC(),
+		time.Now().UTC(),
+		image.OriginalName,
+		image.MimeType,
+	); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (repo ImageRepository) Update(image model.Image) error {
+	if _, err := repo.db.Exec(
+		"UPDATE images SET title = $1, description = $2, updated_at = $3 WHERE id = $4",
+		image.Title,
+		image.Description,
+		time.Now().UTC(),
+		image.Id,
+	); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (repo ImageRepository) Delete(image model.Image) error {
+	if _, err := repo.db.Exec(
+		"DELETE FROM images WHERE id = $1",
+		image.Id,
+	); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/templates/_base.html b/templates/_base.html
deleted file mode 100644
index 9bab238..0000000
--- a/templates/_base.html
+++ /dev/null
@@ -1,4 +0,0 @@
-{{template "header" .}}
-{{template "navigation" .User}}
-{{template "content" .}}
-{{template "footer" .}}
\ No newline at end of file
diff --git a/templates/_footer.html b/templates/_footer.html
deleted file mode 100644
index 18fdea3..0000000
--- a/templates/_footer.html
+++ /dev/null
@@ -1,4 +0,0 @@
-{{define "footer"}}
-<script src="/assets/js/component/fake-input.js"></script>
-<script src="/assets/js/component/copy.js"></script>
-{{end}}
\ No newline at end of file
diff --git a/templates/_header.html b/templates/_header.html
deleted file mode 100644
index 90a099f..0000000
--- a/templates/_header.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{{define "header"}}
-<!DOCTYPE html>
-<meta charset="utf-8">
-<title>{{template "title" .}}</title>
-
-<link rel="shortcut icon" href="/favicon.png">
-<link rel="shortcut icon" href="/favicon.svg">
-
-<meta name="generator" content="Human v1.0">
-
-<meta name="referrer" content="origin">
-
-<meta http-equiv="X-UA-Compatible" content="IE=edge">
-<meta name="HandheldFriendly" content="True">
-<meta name="apple-mobile-web-app-capable" content="yes">
-
-<meta name="viewport" content="user-scalable=no, initial-scale=1.0, maximum-scale=1.0, width=device-width">
-
-<meta name="theme-color" content="#FFC107">
-<meta name="msapplication-navbutton-color" content="#FFC107">
-<meta name="apple-mobile-web-app-status-bar-style" content="#FFC107">
-
-<link href="/assets/css/style.css" rel="stylesheet">
-<link href="/assets/css/fonts.css" rel="stylesheet">
-{{end}}
\ No newline at end of file
diff --git a/templates/_navigation.html b/templates/_navigation.html
deleted file mode 100644
index bca2fc8..0000000
--- a/templates/_navigation.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{{define "navigation"}}
-<nav>
-    <ul>
-        <li class="title"><a href="/"><img src="/assets/images/logo.svg"></a></li>
-    {{if .HasRole "imghost:user" }}
-        <li><a href="/upload">Upload</a></li>
-    {{end}}
-        <li class="spacer"></li>
-    {{if .HasRole "imghost:user" }}
-        <li class="images"><a href="/me/images">My Images</a></li>
-        <li class="albums"><a href="/me/albums">My Albums</a></li>
-        <li class="me"><a href="https://accounts.kuschku.de/profile">{{.Name}}</a></li>
-    {{else if .Id }}
-        <li class="me"><a href="https://accounts.kuschku.de/profile">{{.Name}}</a></li>
-    {{else}}
-        <li><a href="/me/images">Login</a></li>
-    {{end}}
-    </ul>
-</nav>
-{{end}}
\ No newline at end of file
diff --git a/templates/image_detail.html b/templates/image_detail.html
index abd530b..c8e455a 100644
--- a/templates/image_detail.html
+++ b/templates/image_detail.html
@@ -1,56 +1,82 @@
-{{define "title"}}{{.Image.Title}} | ik8r{{end}}
-{{define "content"}}
-<div class="page image detail">
-    <div class="detail">
-    {{if .IsMine}}
-        <h2 class="title fake-input" contenteditable="true" placeholder="Title">{{.Image.Title}}</h2>
-    {{else}}
-        <h2 class="title">{{.Image.Title}}</h2>
-    {{end}}
-        <a class="image" href="/{{.Image.Id}}.png">
-            <img src="/{{.Image.Id}}.png">
-        </a>
-    {{if .IsMine}}
-        <p class="description fake-input" contenteditable="true" placeholder="Description"
-           data-multiline>{{.Image.Description}}</p>
-    {{else}}
-        <div class="description">{{.Image.Description}}</div>
-    {{end}}
-    </div>
-    <div class="sidebar">
-    {{if .IsMine}}
-        <div class="actions">
-            <form name="delete" class="delete-form" method="post">
-                <input type="hidden" name="action" value="delete">
-                <input type="hidden" name="id" value="{{.Image.Id}}">
-                <input type="submit" value="Delete">
-            </form>
-            <form name="upload" class="update-form" method="post">
-                <input type="hidden" name="action" value="update">
-                <input type="hidden" name="id" value="{{.Image.Id}}">
-                <input type="hidden" name="title" value="{{.Image.Title}}">
-                <input type="hidden" name="description" value="{{.Image.Description}}">
-                <input type="submit" id="save" value="Save">
-            </form>
-        </div>
-    {{end}}
-        <div class="url">
-            <p>Detail Page</p>
-            <div>
-                <input id="url_full" type="text" value="https://i.k8r.eu/i/{{.Image.Id}}">
-                <button class="copy" data-target="#url_full">Copy</button>
-            </div>
-        </div>
-        <div class="url">
-            <p>Direct Link</p>
-            <div>
-                <input id="url_direct" type="text" value="https://i.k8r.eu/{{.Image.Id}}.png">
-                <button class="copy" data-target="#url_direct">Copy</button>
-            </div>
-        </div>
-    </div>
-</div>
-{{if .IsMine}}
-<script src="/assets/js/page_image_detail.js"></script>
-{{end}}
-{{end}}
+{{- /*gotype: git.kuschku.de/justjanne/imghost-frontend.ImageDetailData*/ -}}
+<form>
+  <h1>User</h1>
+  <p>
+    <label>
+      Id:<br>
+      <input type="text" disabled value="{{.User.Id}}">
+    </label>
+  </p>
+  <p>
+    <label>
+      Name:<br>
+      <input type="text" disabled value="{{.User.Name}}">
+    </label>
+  </p>
+  <p>
+    <label>
+      Email:<br>
+      <input type="text" disabled value="{{.User.Email}}">
+    </label>
+  </p>
+  <p><b>Roles: </b></p>
+  <ul>
+      {{range .User.Roles}}
+        <li>{{.}}</li>
+      {{end}}
+  </ul>
+</form>
+<form method="post">
+  <h1>Image</h1>
+  <p>
+    <label>
+      Id:<br>
+      <input type="text" disabled value="{{.Image.Id}}">
+    </label>
+  </p>
+  <p>
+    <label>
+      Owner:<br>
+      <input type="text" disabled value="{{.Image.Owner}}">
+    </label>
+  </p>
+  <p>
+    <label>
+      Title:<br>
+      <input type="text" name="title" value="{{.Image.Title}}">
+    </label>
+  </p>
+  <p>
+    <label>
+      Description:<br>
+      <input type="text" name="description" value="{{.Image.Description}}">
+    </label>
+  </p>
+  <p>
+    <label>
+      OriginalName:<br>
+      <input type="text" disabled value="{{.Image.OriginalName}}">
+    </label>
+  </p>
+  <p>
+    <label>
+      MimeType:<br>
+      <input type="text" disabled value="{{.Image.MimeType}}">
+    </label>
+  </p>
+  <p>
+    <label>
+      CreatedAt:<br>
+      <input type="text" disabled value="{{.Image.CreatedAt}}">
+    </label>
+  </p>
+  <p>
+    <label>
+      UpdatedAt:<br>
+      <input type="text" disabled value="{{.Image.UpdatedAt}}">
+    </label>
+  </p>
+  <p>
+    <input type="submit">
+  </p>
+</form>
diff --git a/templates/image_list.html b/templates/image_list.html
deleted file mode 100644
index 4cc941a..0000000
--- a/templates/image_list.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{{define "title"}}My Images | ik8r{{end}}
-{{define "content"}}
-<div class="page image list">
-{{range .Images}}
-    <a class="image" href="/i/{{.Id}}">
-        <div class="image-container">
-            <img src="/{{.Id}}t.png">
-        </div>
-        <div class="info">
-            <p>{{.OriginalName}}</p>
-            <p>
-                <time>{{.CreatedAt.Format "2006-01-02 15:04"}}</time>
-            </p>
-            <p>{{.MimeType}}</p>
-        </div>
-    </a>
-{{end}}
-</div>
-{{end}}
\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
deleted file mode 100644
index 3590730..0000000
--- a/templates/index.html
+++ /dev/null
@@ -1,2 +0,0 @@
-{{define "title"}}ik8r{{end}}
-{{define "content"}}{{end}}
\ No newline at end of file
diff --git a/templates/upload.html b/templates/upload.html
deleted file mode 100644
index 6508d46..0000000
--- a/templates/upload.html
+++ /dev/null
@@ -1,26 +0,0 @@
-{{define "title"}}Upload | ik8r{{end}}
-{{define "content"}}
-<div class="page upload">
-    <div class="container centered">
-        <form class="upload" action="/upload/" method="POST" enctype="multipart/form-data">
-            <label>
-                <span class="text">Select Files</span>
-                <input type="file" name="file" accept=".jpg,.jpeg,.png,.gif,.apng,.tiff,.tif,.bmp,.webp,.mp4,.mov" multiple/>
-            </label>
-        </form>
-    </div>
-    <div class="uploading-images">
-        <div class="images"></div>
-        <div class="sidebar">
-            <div class="url">
-                <p>Album</p>
-                <div>
-                    <input id="url_full" type="text" value="https://i.k8r.eu/a/ERROR">
-                    <button class="copy" data-target="#url_full">Copy</button>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>
-<script src="/assets/js/page_upload.js"></script>
-{{end}}
\ No newline at end of file
diff --git a/types.go b/types.go
index 2ea713f..5cea91b 100644
--- a/types.go
+++ b/types.go
@@ -2,18 +2,10 @@ package main
 
 import (
 	"encoding/json"
+	"github.com/justjanne/imgconv"
 	"os"
-	"time"
 )
 
-type Image struct {
-	Id           string `json:"id"`
-	Title        string
-	Description  string
-	CreatedAt    time.Time
-	OriginalName string
-	MimeType     string `json:"mime_type"`
-}
 
 type Result struct {
 	Id      string   `json:"id"`
@@ -21,25 +13,9 @@ type Result struct {
 	Errors  []string `json:"errors"`
 }
 
-type Size struct {
-	Width  uint   `json:"width"`
-	Height uint   `json:"height"`
-	Format string `json:"format"`
-}
-
-const (
-	sizeFormatCover   = "cover"
-	sizeFormatContain = "contain"
-)
-
-type Quality struct {
-	CompressionQuality uint      `json:"compression_quality"`
-	SamplingFactors    []float64 `json:"sampling_factors"`
-}
-
 type SizeDefinition struct {
-	Size   Size   `json:"size"`
-	Suffix string `json:"suffix"`
+	Size   imgconv.Size `json:"size"`
+	Suffix string       `json:"suffix"`
 }
 
 type RedisConfig struct {
@@ -54,7 +30,7 @@ type DatabaseConfig struct {
 
 type Config struct {
 	Sizes         []SizeDefinition
-	Quality       Quality
+	Quality       imgconv.Quality
 	SourceFolder  string
 	TargetFolder  string
 	Redis         RedisConfig
@@ -66,8 +42,8 @@ type Config struct {
 func NewConfigFromEnv() Config {
 	config := Config{}
 
-	json.Unmarshal([]byte(os.Getenv("IK8R_SIZES")), &config.Sizes)
-	json.Unmarshal([]byte(os.Getenv("IK8R_QUALITY")), &config.Quality)
+	_ = json.Unmarshal([]byte(os.Getenv("IK8R_SIZES")), &config.Sizes)
+	_ = json.Unmarshal([]byte(os.Getenv("IK8R_QUALITY")), &config.Quality)
 	config.SourceFolder = os.Getenv("IK8R_SOURCE_FOLDER")
 	config.TargetFolder = os.Getenv("IK8R_TARGET_FOLDER")
 	config.Redis.Address = os.Getenv("IK8R_REDIS_ADDRESS")
diff --git a/util.go b/util.go
index 0a69e89..d7de6d3 100644
--- a/util.go
+++ b/util.go
@@ -1,31 +1,16 @@
 package main
 
 import (
-	"fmt"
-	"net/http"
-	"html/template"
-	"time"
 	"database/sql"
-	"github.com/go-redis/redis"
-	"strings"
 	"encoding/json"
+	"fmt"
+	"git.kuschku.de/justjanne/imghost-frontend/model"
+	"github.com/go-redis/redis"
+	"html/template"
+	"net/http"
 )
 
-type UserInfo struct {
-	Id    string
-	Name  string
-	Email string
-	Roles []string
-}
 
-func (info UserInfo) HasRole(role string) bool {
-	for _, r := range info.Roles {
-		if r == role {
-			return true
-		}
-	}
-	return false
-}
 
 type PageContext struct {
 	Config      *Config
@@ -35,32 +20,26 @@ type PageContext struct {
 	AssetServer http.Handler
 }
 
-type AlbumImage struct {
-	Id          string
-	Title       string
-	Description string
-	Position    int
-}
-
-type Album struct {
-	Id          string
-	Title       string
-	Description string
-	CreatedAt   time.Time
-	Images      []AlbumImage
-}
-
-func parseUser(r *http.Request) UserInfo {
-	return UserInfo{
-		r.Header.Get("X-Auth-Subject"),
-		r.Header.Get("X-Auth-Username"),
-		r.Header.Get("X-Auth-Email"),
-		strings.Split(r.Header.Get("X-Auth-Roles"), ","),
+func parseUser(r *http.Request) model.User {
+	return model.User{
+		"ad45284c-be4d-4546-8171-41cf126ac091",
+		"justJanne",
+		"janne@kuschku.de",
+		[]string{"imghost:user", "imghost:admin"},
 	}
+
+	/*
+		return UserInfo{
+			r.Header.Get("X-Auth-Subject"),
+			r.Header.Get("X-Auth-Username"),
+			r.Header.Get("X-Auth-Email"),
+			strings.Split(r.Header.Get("X-Auth-Roles"), ","),
+		}
+	*/
 }
 
 func returnJson(w http.ResponseWriter, data interface{}) error {
-	marshalled, err := json.Marshal(data)
+	marshalled, err := json.MarshalIndent(data, "", "  ")
 	if err != nil {
 		return err
 	}
@@ -84,16 +63,17 @@ func returnError(w http.ResponseWriter, code int, message string) error {
 
 func formatTemplate(w http.ResponseWriter, templateName string, data interface{}) error {
 	pageTemplate, err := template.ParseFiles(
-		"templates/_base.html",
+		/*"templates/_base.html",
 		"templates/_header.html",
 		"templates/_navigation.html",
-		"templates/_footer.html",
+		"templates/_footer.html",*/
 		fmt.Sprintf("templates/%s", templateName),
 	)
 	if err != nil {
 		return err
 	}
 
+	w.Header().Set("Content-Type", "text/html")
 	err = pageTemplate.Execute(w, data)
 	if err != nil {
 		return err
@@ -101,3 +81,33 @@ func formatTemplate(w http.ResponseWriter, templateName string, data interface{}
 
 	return nil
 }
+
+func (ctx *PageContext) getImageList(user model.User) ([]model.Image, error) {
+	result, err := ctx.Database.Query(`
+			SELECT
+				id,
+				coalesce(title,  ''),
+				coalesce(description, ''),
+        		coalesce(created_at, to_timestamp(0)),
+				coalesce(original_name, ''),
+				coalesce(type, '')
+			FROM images
+			WHERE owner = $1
+			ORDER BY created_at DESC
+			`, user.Id)
+	if err != nil {
+		return nil, err
+	}
+
+	var images []model.Image
+	for result.Next() {
+		var info model.Image
+
+		if err := result.Scan(&info.Id, &info.Title, &info.Description, &info.CreatedAt, &info.OriginalName, &info.MimeType); err != nil {
+			return nil, err
+		}
+		images = append(images, info)
+	}
+
+	return images, nil
+}
-- 
GitLab