From f3756dac8a49f3b5599fd50f4c631da4168e9eb0 Mon Sep 17 00:00:00 2001 From: Nathan Reiner Date: Mon, 24 Nov 2025 21:13:47 +0100 Subject: add jump-to month --- src/routes/api/auth/create-user.zig | 29 ----------------------------- src/routes/api/auth/root.zig | 1 - src/routes/api/profile/create.zig | 29 +++++++++++++++++++++++++++++ src/routes/api/profile/list.zig | 17 +++++++++++++++++ src/routes/api/profile/root.zig | 2 ++ src/storage/user.zig | 18 ++++++++++++++++++ static/index.js | 11 +++++++---- static/month.js | 10 ++++++++-- static/pages/image-viewer/index.js | 17 ++++++++++------- static/widgets/image/index.js | 11 +++++++++-- static/widgets/month-select/index.css | 9 +++++++++ static/widgets/month-select/index.js | 11 +++++++++++ 12 files changed, 120 insertions(+), 45 deletions(-) delete mode 100644 src/routes/api/auth/create-user.zig create mode 100644 src/routes/api/profile/create.zig create mode 100644 src/routes/api/profile/list.zig diff --git a/src/routes/api/auth/create-user.zig b/src/routes/api/auth/create-user.zig deleted file mode 100644 index 6bd9a82..0000000 --- a/src/routes/api/auth/create-user.zig +++ /dev/null @@ -1,29 +0,0 @@ -const std = @import("std"); - -const memora = @import("memora"); -const Context = memora.Context; -const Storage = memora.Storage; - -pub const access = .admins; - -pub const Body = struct { - name: []const u8, - full_name: []const u8, - birthday: []const u8, - password: []const u8, -}; - -pub fn post(ctx: *Context, body: Body) anyerror!void { - var user = try Storage.User.new( - ctx.storage, - body.name, - body.full_name, - body.birthday, - body.password, - false, - ctx.storage.allocator, - ); - defer user.deinit(); - - try user.sync(); -} diff --git a/src/routes/api/auth/root.zig b/src/routes/api/auth/root.zig index fa93c92..5f45891 100644 --- a/src/routes/api/auth/root.zig +++ b/src/routes/api/auth/root.zig @@ -2,4 +2,3 @@ const HandlerInfo = @import("../../handler-info.zig"); pub const login: HandlerInfo = .from_type(@import("login.zig")); pub const @"first-login": HandlerInfo = .from_type(@import("first-login.zig")); -pub const @"create-user": HandlerInfo = .from_type(@import("create-user.zig")); diff --git a/src/routes/api/profile/create.zig b/src/routes/api/profile/create.zig new file mode 100644 index 0000000..68f32ec --- /dev/null +++ b/src/routes/api/profile/create.zig @@ -0,0 +1,29 @@ +const std = @import("std"); + +const memora = @import("memora"); +const Context = memora.Context; +const Storage = memora.Storage; + +pub const access = .admins; + +const Body = struct { + name: []const u8, + full_name: []const u8, + birthday: []const u8, + password: []const u8, +}; + +pub fn post(ctx: *Context, body: Body) !void { + var user = try Storage.User.new( + ctx.storage, + body.name, + body.full_name, + body.birthday, + body.password, + true, + ctx.storage.allocator, + ); + defer user.deinit(); + + try user.sync(); +} diff --git a/src/routes/api/profile/list.zig b/src/routes/api/profile/list.zig new file mode 100644 index 0000000..59d4684 --- /dev/null +++ b/src/routes/api/profile/list.zig @@ -0,0 +1,17 @@ +const std = @import("std"); + +const memora = @import("memora"); +const Context = memora.Context; +const Storage = memora.Storage; + +pub const access = .admins; + +const Response = struct { + users: [][]const u8, +}; + +pub fn get(ctx: *Context) !Response { + return .{ + .users = try Storage.User.list(ctx.storage, ctx.allocator) + }; +} diff --git a/src/routes/api/profile/root.zig b/src/routes/api/profile/root.zig index 0bdd064..45dec8e 100644 --- a/src/routes/api/profile/root.zig +++ b/src/routes/api/profile/root.zig @@ -4,3 +4,5 @@ const HandlerInfo = memora.routes.HandlerInfo; pub const image = @import("image/root.zig"); pub const set: HandlerInfo = .from_type(@import("set.zig")); pub const @"update-password": HandlerInfo = .from_type(@import("update-password.zig")); +pub const create: HandlerInfo = .from_type(@import("create.zig")); +pub const list: HandlerInfo = .from_type(@import("list.zig")); diff --git a/src/storage/user.zig b/src/storage/user.zig index b817f84..c2cc82a 100644 --- a/src/storage/user.zig +++ b/src/storage/user.zig @@ -214,3 +214,21 @@ pub fn set_image( try file_writer.interface.flush(); } + +pub fn list( + storage: *Storage, + allocator: std.mem.Allocator, +) ![][]const u8 { + var user = try storage.dir.openDir("user", .{ .iterate = true }); + defer user.close(); + + var ids: std.ArrayList([]const u8) = .empty; + + var iterator = user.iterate(); + + while (try iterator.next()) |entry| { + try ids.append(allocator, try allocator.dupe(u8, entry.name)); + } + + return ids.toOwnedSlice(allocator); +} diff --git a/static/index.js b/static/index.js index c5bfccc..e67d234 100644 --- a/static/index.js +++ b/static/index.js @@ -1,6 +1,6 @@ import * as sfw from 'sfw'; import * as api from 'api'; -import { literal as m } from './month.js'; +import Month from './month.js'; import * as service_worker from './service-worker/index.js'; import LoginView from './pages/login/index.js'; @@ -19,8 +19,12 @@ const image_viewer = ImageViewer.new(); const reload = () => { image_viewer.clear(); api.images.list().then(images => { + const last = Month.from_unix(images[0].timestamp); + let first = Month.from_unix(images.findLast(i => i.timestamp).timestamp); + month_select.months = first.to(last); + for (const image of images) { - image_viewer.add(image.id); + image_viewer.add(image); } }); } @@ -50,9 +54,8 @@ const search = Search.new({ }); const month_select = MonthSelect.new({ - months: m`2019-08`.to(m`2025-11`), + onmonth: (month) => image_viewer.jump_to(month), }); - const upload_bar = UploadBar.new(); const settings = SettingsView.new({ diff --git a/static/month.js b/static/month.js index 7875f36..05037d4 100644 --- a/static/month.js +++ b/static/month.js @@ -55,7 +55,7 @@ export default class Month { } is_same(other) { - return this.year == other.year || this.month == other.month; + return this.year == other?.year && this.month == other?.month; } is_same_year(other) { @@ -68,8 +68,14 @@ export default class Month { return date.toLocaleDateString('default', { month: 'long' }) } + static from_unix(date) { + if (date == null) return null; + + return Month.from_date(new Date(date * 1000)); + } + static from_date(date) { - return new Month(date.getMonth() - 1, date.getFullYear()); + return new Month(date.getMonth() + 1, date.getFullYear()); } static from_string(string) { diff --git a/static/pages/image-viewer/index.js b/static/pages/image-viewer/index.js index 72af671..73b824e 100644 --- a/static/pages/image-viewer/index.js +++ b/static/pages/image-viewer/index.js @@ -15,9 +15,6 @@ export default class ImageViewer extends sfw.element.Container { this.#images = []; - this.onnewer = () => {} - this.onolder = () => {} - this.body.append( this.#container = Div.new({ id: 'container', @@ -32,8 +29,8 @@ export default class ImageViewer extends sfw.element.Container { ); } - add(id) { - const image = Image.new({ id }); + add(metadata) { + const image = Image.new({ metadata }); this.#images.push(image); this.#container.insertBefore(image, this.#subtitle); @@ -45,10 +42,16 @@ export default class ImageViewer extends sfw.element.Container { this.#container.append(this.#subtitle); } + jump_to(month) { + const image = this.#images.find(i => i.month == month || i.month.is_same(month)); + image.scrollIntoView({ behavior: 'instant' }) + } + #preload(image) { - const loading_zone = (this.#container.scrollTop + this.#container.offsetHeight) * 2; + const loading_zone_after = (this.#container.scrollTop + this.#container.offsetHeight) * 2; + const loading_zone_before = this.#container.scrollTop - (this.#container.offsetHeight - image.offsetHeight) * 2; - if (image.offsetTop < loading_zone) { + if (loading_zone_before < image.offsetTop && image.offsetTop < loading_zone_after) { image.load(); } } diff --git a/static/widgets/image/index.js b/static/widgets/image/index.js index 4c25988..291cf23 100644 --- a/static/widgets/image/index.js +++ b/static/widgets/image/index.js @@ -1,5 +1,6 @@ import * as api from 'api'; import * as sfw from 'sfw'; +import Month from '../../month.js'; const { Div, Img } = sfw.element.native; const css = await sfw.css(import.meta.url, './index.css') @@ -9,6 +10,7 @@ export default class Image extends sfw.element.Container { #image #menu #id + #month constructor() { super ({ css }) @@ -43,8 +45,9 @@ export default class Image extends sfw.element.Container { ); } - set id(id) { - this.#id = id; + set metadata(image) { + this.#id = image.id; + this.#month = Month.from_unix(image.timestamp); this.#container.classList.remove('loaded'); } @@ -53,4 +56,8 @@ export default class Image extends sfw.element.Container { this.#image.src = `/api/image/load/${this.#id}`; } } + + get month() { + return this.#month; + } } diff --git a/static/widgets/month-select/index.css b/static/widgets/month-select/index.css index 95a87b0..25ed5d2 100644 --- a/static/widgets/month-select/index.css +++ b/static/widgets/month-select/index.css @@ -56,6 +56,10 @@ position: relative; } +.month-item.none { + font-style: italic; +} + .month-item:before { content: ''; display: block; @@ -84,6 +88,11 @@ height: 27px; } +.month-item.none:before { + top: 0; + height: 20px; +} + .month-item:hover { background: var(--primary); color: var(--fg-primary); diff --git a/static/widgets/month-select/index.js b/static/widgets/month-select/index.js index 6ffbac2..63caf70 100644 --- a/static/widgets/month-select/index.js +++ b/static/widgets/month-select/index.js @@ -70,5 +70,16 @@ export default class MonthSelect extends sfw.element.Container { last = month; } + + this.#month_container.append( + Div.new({ + className: 'month-item none', + innerText: 'No Date', + onclick: () => { + this.hide(); + this.onmonth(null); + } + }) + ); } } -- cgit v1.2.3-70-g09d2