Author: Adam <git@apiote.tk>
show film experiences
datastructure/experiences.go | 41 ++++++++++++++++ db/db.go | 55 ++++++++++++++++++++++ front/capnproto.go | 4 + front/html.go | 10 ++++ front/renderer.go | 1 i18n/en-GB.toml | 1 libamuse/experiences.go | 71 +++++++++++++++++++++++++++++ router.go | 7 ++ static/style/style.css | 8 +++ templates/experiences.html | 92 ++++++++++++++++++++++++++++++++++++++
diff --git a/datastructure/experiences.go b/datastructure/experiences.go new file mode 100644 index 0000000000000000000000000000000000000000..0e4c3ff42140f068f8862caa5d3fb7c8f768f797 --- /dev/null +++ b/datastructure/experiences.go @@ -0,0 +1,41 @@ +package datastructure + +import ( + "notabug.org/apiote/amuse/i18n" + + "time" +) + +type ExperiencesEntry struct { + ItemInfo + Type string + Id string + Datetime time.Time +} + +type Experiences struct { + List []ExperiencesEntry + Page int + Pages int + Query string +} + +func (e ExperiencesEntry) FormatDatetime(strings i18n.Translation) string { + return i18n.FormatDate(e.Datetime, strings.Global["date_format_time"], strings.Global) +} + +func (e Experiences) NextPage() int { + if e.Page < e.Pages { + return e.Page + 1 + } else { + return e.Page + } +} + +func (e Experiences) PrevPage() int { + if e.Page > 1 { + return e.Page - 1 + } else { + return e.Page + } +} diff --git a/db/db.go b/db/db.go index 9a3c8f6fbd0608f4bd1ab680aa7169a33192ac88..bc112ed62e6cc8fe96acf68117bc3f1e890ba681 100644 --- a/db/db.go +++ b/db/db.go @@ -584,3 +584,58 @@ } return watchlist, nil } + +func GetUserExperiences(username, filter string, page int) (datastructure.Experiences, error) { + experiences := datastructure.Experiences{} + db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db") + if err != nil { + fmt.Fprintf(os.Stderr, "DB open err\n") + return experiences, err + } + defer db.Close() + + if page <= 0 { + page = 1 + } + + var pages float64 + row := db.QueryRow(`select count(*) from experiences where username = ?`, username) + err = row.Scan(&pages) + if err != nil { + return experiences, err + } + experiences.Pages = int(math.Ceil(pages / 18)) + + offset := (page - 1) * 18 + + //todo filter, order by + + var whereClause string + if filter != "" { + whereClause = "and c1.title like '%" + filter + "%'" + } + + rows, err := db.Query(`select item_id, item_type, cover, status, title, year_start, based_on, genres, runtime, part, time from experiences natural join item_cache where username = ? `+whereClause+` order by time desc limit ?,18;`, username, offset) + + if err != nil { + fmt.Fprintf(os.Stderr, "Select err: %v\n", err) + return experiences, err + } + defer rows.Close() + + for rows.Next() { + var ( + entry datastructure.ExperiencesEntry + ) + err := rows.Scan(&entry.Id, &entry.Type, &entry.Cover, &entry.Status, &entry.Title, &entry.YearStart, &entry.BasedOn, &entry.Genres, &entry.Runtime, &entry.Part, &entry.Datetime) + if err != nil { + fmt.Println("Scan error") + return datastructure.Experiences{}, err + } + + experiences.List = append(experiences.List, entry) + } + + return experiences, nil + +} diff --git a/front/capnproto.go b/front/capnproto.go index 3d87d7b473c763b8789d757fb46a33f5478a411f..b2656f82d2d1f393f25a434b94740ce0a079814e 100644 --- a/front/capnproto.go +++ b/front/capnproto.go @@ -58,3 +58,7 @@ func (CapnprotoRenderer) RenderWatchlist(watchlist datastructure.Watchlist, languages []language.Tag) string { return TODO("implement CapnprotoRenderer.RenderWatchlist").(string) } + +func (CapnprotoRenderer) RenderExperiences(experiences datastructure.Experiences, languages []language.Tag) string { + return TODO("implement CapnprotoRenderer.RenderExperiences").(string) +} diff --git a/front/html.go b/front/html.go index 9f3c0d199770332eef67a18153d830b1fc5ca1e2..3e96480f9dd37dba02c8c689e0f56c57233968ea 100644 --- a/front/html.go +++ b/front/html.go @@ -40,6 +40,10 @@ func (d RenderData) FormatDate(date time.Time) string { return i18n.FormatDate(date, d.Strings.Global["date_format"], d.Strings.Global) } +func (d RenderData) FormatDateNice(date time.Time, timezone string) string { + return i18n.FormatDateNice(date, d.Strings, timezone) +} + func (d RenderData) RenderAsciiDoc(s string) template.HTML { return i18n.RenderAsciiDoc(s) } @@ -139,3 +143,9 @@ data := RenderData{Data: watchlist} data.State.User = r.user return render(languages, data, "watchlist") } + +func (r HtmlRenderer) RenderExperiences(experiences datastructure.Experiences, languages []language.Tag) string { + data := RenderData{Data: experiences} + data.State.User = r.user + return render(languages, data, "experiences") +} diff --git a/front/renderer.go b/front/renderer.go index 9c083c6e7cbe6fb5b35367825d986613462d0cd6..bb4b743bd5b287c25f4e50b395fe47ed12ed76f8 100644 --- a/front/renderer.go +++ b/front/renderer.go @@ -29,6 +29,7 @@ RenderAbout([]language.Tag) string RenderErrorPage(int, []language.Tag) string RenderLogin([]language.Tag, error, string) string RenderWatchlist(datastructure.Watchlist, []language.Tag) string + RenderExperiences(datastructure.Experiences, []language.Tag) string } func NewRenderer(mimetype string, user accounts.User) (Renderer, error) { diff --git a/i18n/en-GB.toml b/i18n/en-GB.toml index 3c46225631b4d5c5b863400b1bd5f8d422091031..eb0309044f33f609afb6958759598b2b49214590 100644 --- a/i18n/en-GB.toml +++ b/i18n/en-GB.toml @@ -11,6 +11,7 @@ [global] # format as per POSIX date(1p) without %c, %r, %x, %X date_format = "%d %B %Y" date_format_full = "%d %B %Y, %H:%M" +date_format_time = "%H:%M" experience_format_today = "today" experience_format_yesterday = "yesterday" diff --git a/libamuse/experiences.go b/libamuse/experiences.go new file mode 100644 index 0000000000000000000000000000000000000000..fecffbc08697ad7310e7f2472693fb62f08eb8fd --- /dev/null +++ b/libamuse/experiences.go @@ -0,0 +1,71 @@ +package libamuse + +import ( + "notabug.org/apiote/amuse/accounts" + "notabug.org/apiote/amuse/datastructure" + "notabug.org/apiote/amuse/db" + + "notabug.org/apiote/gott" + + "time" +) + +func getExperiences(args ...interface{}) (interface{}, error) { + request := args[0].(*RequestData) + result := args[1].(*Result) + page := args[2].(int) + experiences, err := db.GetUserExperiences(result.user.Username, request.id, page) + result.result = experiences + + return gott.Tuple(args), err +} + +func parseExperienceDates(args ...interface{}) interface{} { + result := args[1].(*Result) + experiences := result.result.(datastructure.Experiences) + location, _ := time.LoadLocation(result.user.Timezone) + for i, experience := range experiences.List { + experience.Datetime = experience.Datetime.In(location) + experiences.List[i] = experience + } + result.result = experiences + + return gott.Tuple(args) +} + +func renderExperiences(args ...interface{}) interface{} { + request := args[0].(*RequestData) + result := args[1].(*Result) + page := args[2].(int) + experiences := result.result.(datastructure.Experiences) + experiences.Page = page + experiences.Query = request.id + result.page = result.renderer.RenderExperiences(experiences, result.languages) + + return gott.Tuple(args) +} + +func ShowExperiences(username string, auth accounts.Authentication, languages, mimetype, filter string, page int) (string, error) { + auth.Necessary = true + if page <= 0 { + page = 1 + } + + request := &RequestData{id: filter, language: languages, mimetype: mimetype, auth: auth, username: username} + r, err := gott. + NewResult(gott.Tuple{request, &Result{}, page}). + Bind(parseLanguage). + Bind(verifyToken). + Bind(verifyUser). + Bind(getExperiences). + Map(parseExperienceDates). + Bind(createRenderer). + Map(renderExperiences). + Finish() + + if err != nil { + return "", err + } else { + return r.(gott.Tuple)[1].(*Result).page, nil + } +} diff --git a/router.go b/router.go index 3047e0319638c6f45e182a87c722e8e0b035d3ef..d395cf8677d7a01d89895e384a4860e7766f7a65 100644 --- a/router.go +++ b/router.go @@ -320,7 +320,12 @@ } func userExperiences(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages string, mimetype string) { if r.Method == "" || r.Method == "GET" { - // todo + var page int + r.ParseForm() + filter := r.Form.Get("filter") + fmt.Sscanf(r.Form.Get("page"), "%d", &page) + experiences, err := libamuse.ShowExperiences(username, auth, acceptLanguages, mimetype, filter, page) + render(experiences, err, w, acceptLanguages, mimetype) } else if r.Method == "POST" { r.ParseForm() itemId := r.PostForm.Get("itemId") diff --git a/static/style/style.css b/static/style/style.css index 1f5d73f48342cad85ab13e64a6cf3c5495f32f14..932095c6bc9ffb7fdd54d05e828a5fe6be7f2763 100644 --- a/static/style/style.css +++ b/static/style/style.css @@ -301,6 +301,10 @@ .margin-top-0 { margin-top: 0; } +.margin-top-_25 { + margin-top: .25rem; +} + .margin-top-1 { margin-top: 1rem; } @@ -311,6 +315,10 @@ } .margin-bottom-_5 { margin-bottom: .5rem; +} + +.margin-bottom-1 { + margin-bottom: 1rem; } .margin-bottom-2 { diff --git a/templates/experiences.html b/templates/experiences.html new file mode 100644 index 0000000000000000000000000000000000000000..8b34d1b9650afd79c4a691cca7e0fe7a839f7476 --- /dev/null +++ b/templates/experiences.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>{{.State.User.Username}}’s experiences — a·muse</title> + <link rel="stylesheet" href="/static/style/style.css" /> + <link rel="icon" type="image/svg+xml" href="/static/img/logo.svg"> + <link rel="apple-touch-icon" type="image/svg+xml" href="/static/img/logo.svg"> + </head> + <body> + <header class="w12 padding-bottom-_25 flex flex-row flex-justify-space flex-align-centre"> + <a href="/" class="decoration-none"> + <h1 class="inline valign-mid text sans margin-lr-1">a·muse</h1> + </a> + <div class="margin-lr-1 text"> + <nav> + <label for="hamburger" class="cursor-hand"> + <img src="/users/{{.State.User.Username}}/avatar?size=small" class="border-radius-25 width-1_5"/> + </label> + <input type="checkbox" id="hamburger" class="display-none" /> + <ul class="absolute right top-1 padding-lr-1 padding-tb-_5 bg align-right list-style-none sans"> + <li><a href="/users/{{.State.User.Username}}" class="decoration-none text-accent">Profile</a><span class="material-icon padding-lr-_5"></span></li> + <li><a href="/users/{{.State.User.Username}}/watchlist" class="decoration-none text-accent">Watchlist</a><span class="material-icon padding-lr-_5"></span></li> + <li><a href="/users/{{.State.User.Username}}/readlist" class="decoration-none text-accent">Readlist</a><span class="material-icon padding-lr-_5"></span></li> + <li><a href="/users/{{.State.User.Username}}/experiences" class="decoration-none text-accent">Experiences</a><span class="material-icon padding-lr-_5"></span></li> + <li class="bg-error"> + <form action="/users/{{.State.User.Username}}/sessions/{{.State.User.Session}}" method="POST" class="inline"> + <input type="hidden" value="DELETE" name="method" /> + <input type="submit" value="Log out" class="border-none bg-none font-normal text-accent padding-lr-0 cursor-hand font-1" /> + </form><span class="material-icon padding-lr-_5"></span> + </li> + </ul> + </nav> + </div> + </header> + <main class="margin-lr-1"> + <!-- search, filter, order --> + <div class="flex flex-row flex-wrap flex-centre flex-align-start margin-top-1"> + <form method="GET" class="flex inline margin-lr-1 border-bottom"> + <input type="search" name="filter" class="border-none bg-none sans text" placeholder="filter experiences" value="{{.Data.Query}}" /> + </form> + </div> + <div class="flex flex-row flex-wrap flex-justify-space flex-align-start margin-top-1"> + <div> + {{if gt .Data.Page 1}} + <a href="/users/{{.State.User.Username}}/experiences?filter={{.Data.Query}}&page={{.Data.PrevPage}}" class="decoration-none" title="{{.Strings.Search.prev_link_title}}"><span class="material-icon font-2"></span></a> + {{end}} + </div> + <div> + {{if lt .Data.Page .Data.Pages}} + <a href="/users/{{.State.User.Username}}/experiences?filter={{.Data.Query}}&page={{.Data.NextPage}}" class="decoration-none" title="{{.Strings.Search.next_link_title}}"><span class="material-icon font-2"></span></a> + {{end}} + </div> + </div> + <div class="flex flex-align-start"> + {{- $lastDate:="" -}} + {{- range .Data.List -}} + {{- if ne $lastDate ($.FormatDate .Datetime)}} + <div class="w12 margin-lr-5"> + <span class="sans">{{$.FormatDateNice .Datetime $.State.User.Timezone}}</span><hr class="margin-top-_25 margin-bottom-1"/> + {{- end -}} + <p> + <span class="sans">{{.FormatDatetime $.Strings}}</span> + <a href="/{{.Type}}s/{{.Id}}" class="sans decoration-none">{{.Title}} ({{.YearStart}})</a> + <!-- if series then with item_id=id/code show code --> + {{- if gt .Collection 0 -}} + ({{.Collection}} <!-- collection name and link --> #{{.Part}}) + {{- end -}} + </p> + {{- if and (ne $lastDate ($.FormatDate .Datetime)) (ne $lastDate "")}} + </div> + {{end}} + {{$lastDate = ($.FormatDate .Datetime) -}} + {{end}} + </div> + </div> + <div class="flex flex-row flex-wrap flex-justify-space flex-align-start margin-top-1"> + <div> + {{if gt .Data.Page 1}} + <a href="/users/{{.State.User.Username}}/experiences?filter={{.Data.Query}}&page={{.Data.PrevPage}}" class="decoration-none" title="{{.Strings.Search.prev_link_title}}"><span class="material-icon font-2"></span></a> + {{end}} + </div> + <div> + {{if lt .Data.Page .Data.Pages}} + <a href="/users/{{.State.User.Username}}/experiences?filter={{.Data.Query}}&page={{.Data.NextPage}}" class="decoration-none" title="{{.Strings.Search.next_link_title}}"><span class="material-icon font-2"></span></a> + {{end}} + </div> + </div> + </main> + </body> +</html>