diff options
author | Andinus <andinus@nand.sh> | 2022-06-09 21:12:03 +0530 |
---|---|---|
committer | Andinus <andinus@nand.sh> | 2022-06-09 21:12:03 +0530 |
commit | 2085b4cac3a86d59360531d48251c8ab39dec0dd (patch) | |
tree | 53cabc820312da2abff2bbb9b44132b61302cad1 | |
parent | 48df36de5aa962b32d3313d6a9d2ace6a5fdac11 (diff) | |
download | crater-2085b4cac3a86d59360531d48251c8ab39dec0dd.tar.gz |
Initial Gallery version
- Handles login, logout, simple directories.
-rw-r--r-- | .cro.yml | 15 | ||||
-rw-r--r-- | .gitignore | 8 | ||||
-rw-r--r-- | META6.json | 31 | ||||
-rw-r--r-- | README.org | 12 | ||||
-rw-r--r-- | bin/crater | 3 | ||||
-rw-r--r-- | lib/Crater/Gallery.rakumod | 25 | ||||
-rw-r--r-- | lib/Crater/Routes.rakumod | 24 | ||||
-rw-r--r-- | lib/Crater/Routes/Auth.rakumod | 34 | ||||
-rw-r--r-- | lib/Crater/Routes/Gallery.rakumod | 25 | ||||
-rw-r--r-- | lib/Crater/Service.rakumod | 52 | ||||
-rw-r--r-- | lib/Crater/Session.rakumod | 7 | ||||
-rw-r--r-- | resources/config.toml | 3 | ||||
-rw-r--r-- | resources/css/colors.css | 215 | ||||
-rw-r--r-- | resources/css/style.css | 104 | ||||
-rw-r--r-- | templates/base.crotmp | 14 | ||||
-rw-r--r-- | templates/gallery.crotmp | 22 | ||||
-rw-r--r-- | templates/login.crotmp | 15 |
17 files changed, 602 insertions, 7 deletions
diff --git a/.cro.yml b/.cro.yml new file mode 100644 index 0000000..a3bc036 --- /dev/null +++ b/.cro.yml @@ -0,0 +1,15 @@ +--- +links: [] +name: crater +entrypoint: lib/Crater/Service.rakumod +endpoints: + - + protocol: http + host-env: CRATER_HOST + id: http + name: HTTP + port-env: CRATER_PORT +id: crater +env: [] +cro: 1 +... diff --git a/.gitignore b/.gitignore index 4a5e4c7..c258360 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -lib/.precomp +# Caches +.precomp + +# Backup files +*~ + +.log diff --git a/META6.json b/META6.json new file mode 100644 index 0000000..559fd1a --- /dev/null +++ b/META6.json @@ -0,0 +1,31 @@ +{ + "name": "crater", + "auth": "zef:andinus", + "version": "0.1.0", + "description": "Crater is a photo gallery", + "authors": ["Andinus <andinus@nand.sh>"], + "license": "ISC", + "perl": "6.d", + "provides": { + "Crater::Service": "lib/Crater/Service.rakumod", + "Crater::Routes": "lib/Crater/Routes.rakumod", + "Crater::Routes::Auth": "lib/Crater/Routes/Auth.rakumod", + "Crater::Routes::Gallery": "lib/Crater/Routes/Gallery.rakumod", + "Crater::Gallery": "lib/Crater/Gallery.rakumod", + "Crater::Session": "lib/Crater/Session.rakumod" + }, + "depends": [ + "DBIish:ver<0.6.5+>:auth<zef:raku-community-modules>", + "Cro::HTTP", + "Cro::WebApp" + ], + "build-depends": [], + "test-depends": [], + "resources": [ + + ], + "tags": [ + "crater", "media", "gallery" + ], + "source-url": "https://github.com/andinus/crater" +} diff --git a/README.org b/README.org index 705d8b6..922daf3 100644 --- a/README.org +++ b/README.org @@ -1,17 +1,17 @@ #+title: Crater -#+subtitle: Crater is a simple student portal written in Raku +#+subtitle: Crater is a photo gallery #+export_file_name: index #+setupfile: ~/.emacs.d/org-templates/projects.org -| Website | https://andinus.nand.sh/crater | -| Source | https://git.tilde.institute/andinus/crater | -| GitHub (mirror) | https://github.com/andinus/crater | +| Website | https://andinus.unfla.me/crater | +| Source | https://git.unfla.me/crater | +| GitHub (mirror) | https://github.com/andinus/crater | * License #+begin_src -Crater - Crater is a simple student portal written in Raku -Copyright (C) 2021, Andinus <andinus@nand.sh> +Crater - Crater is a photo gallery +Copyright (C) 2021, 2022 Andinus <andinus@nand.sh> This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published diff --git a/bin/crater b/bin/crater new file mode 100644 index 0000000..dcd5bdf --- /dev/null +++ b/bin/crater @@ -0,0 +1,3 @@ +#!/usr/bin/env raku + +use Crater::Service; diff --git a/lib/Crater/Gallery.rakumod b/lib/Crater/Gallery.rakumod new file mode 100644 index 0000000..ea92c47 --- /dev/null +++ b/lib/Crater/Gallery.rakumod @@ -0,0 +1,25 @@ +class Crater::Gallery { + has IO $.directory is required; + + method list() { + my @gallery; + for dir($!directory).sort(*.modified) { + if .IO.d { + + } elsif .IO.f { + my Str $ext = .extension.lc; + if $ext eq "jpg"|"png" { + push @gallery, %( :type<img>, :src($_.relative($!directory)) ); + } elsif $ext eq "0" { + push @gallery, %( :type<heading>, :text($_.slurp) ); + } elsif $ext eq "txt" { + push @gallery, %( :type<text>, :text($_.slurp) ); + } else { + warn "Unhandled file :$_"; + } + } + } + + return @gallery; + } +} diff --git a/lib/Crater/Routes.rakumod b/lib/Crater/Routes.rakumod new file mode 100644 index 0000000..6206e5c --- /dev/null +++ b/lib/Crater/Routes.rakumod @@ -0,0 +1,24 @@ +use Cro::HTTP::Router; +use Cro::WebApp::Template; + +use Crater::Gallery; +use Crater::Routes::Auth; +use Crater::Routes::Gallery; + +sub routes( + Crater::Gallery :$gallery!, #= gallery object + Str :$password!, #= password for authentication +) is export { + template-location 'templates/'; + + route { + after { redirect '/login', :see-other if .status == 401 }; + + include auth-routes(:$password); + include gallery-routes(:$gallery); + + get -> 'resources', 'css', *@path { + static 'resources', 'css', @path; + } + } +} diff --git a/lib/Crater/Routes/Auth.rakumod b/lib/Crater/Routes/Auth.rakumod new file mode 100644 index 0000000..23872e9 --- /dev/null +++ b/lib/Crater/Routes/Auth.rakumod @@ -0,0 +1,34 @@ +use Cro::HTTP::Router; +use Cro::WebApp::Template; + +use Crater::Session; + +sub auth-routes( + Str :$password!, #= password for authentication +) is export { + route { + get -> LoggedIn $session, 'login' { + redirect '/', :see-other; + } + get -> Crater::Session $session, 'login' { + template 'login.crotmp', { :!error }; + } + + post -> Crater::Session $session, 'login' { + request-body -> (:$pass!, *%) { + if $password eq $pass { + $session.logged-in = True; + redirect :see-other, '/'; + } else { + template 'login.crotmp', { + error => 'Incorrect password.' + }; + } + } + } + get -> Crater::Session $session, 'logout' { + $session.logged-in = False; + redirect :see-other, '/'; + } + } +} diff --git a/lib/Crater/Routes/Gallery.rakumod b/lib/Crater/Routes/Gallery.rakumod new file mode 100644 index 0000000..04c8d74 --- /dev/null +++ b/lib/Crater/Routes/Gallery.rakumod @@ -0,0 +1,25 @@ +use Cro::HTTP::Router; +use Cro::WebApp::Template; + +use Crater::Gallery; +use Crater::Session; + +sub gallery-routes( + Crater::Gallery :$gallery!, #= gallery object +) is export { + route { + get -> LoggedIn $session { + template 'gallery.crotmp', { + gallery => $gallery.list(), + title => "Gallery" + }; + } + + get -> { + redirect '/login', :see-other; + } + get -> *@path { + static $gallery.directory, @path; + } + } +} diff --git a/lib/Crater/Service.rakumod b/lib/Crater/Service.rakumod new file mode 100644 index 0000000..c0bdd7a --- /dev/null +++ b/lib/Crater/Service.rakumod @@ -0,0 +1,52 @@ +use Config::TOML; + +use Cro::HTTP::Server; +use Cro::HTTP::Log::File; +use Cro::HTTP::Session::InMemory; + +use Crater::Routes; +use Crater::Gallery; +use Crater::Session; + +#| Crater is a photo gallery +sub MAIN( + IO() :$config where *.IO.f = 'resources/config.toml', #= configuration file + IO() :$directory! where *.IO.d, #= gallery directory (takes absolute path) + Str :$password = '0x', #= password for authentication + Bool :$verbose, #= increase verbosity +) is export { + put "Initialized: {now - INIT now}"; + put "Gallery: {$directory.absolute}"; + + my %conf = from-toml($config.slurp); + %conf<server><host> //= %*ENV<CRATER_HOST>; + %conf<server><port> //= %*ENV<CRATER_PORT>; + + my Crater::Gallery $gallery = Crater::Gallery.new(:$directory); + + my Cro::Service $http = Cro::HTTP::Server.new( + http => <1.1>, + host => %conf<server><host> || die("host not set"), + port => %conf<server><port> || die("port not set"), + application => routes(:$password, :$gallery), + before => [ + Cro::HTTP::Session::InMemory[Crater::Session].new( + expiration => Duration.new(60 * 15) + ); + ], + after => [ + Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR) + ] + ); + + $http.start; + say "Listening at http://%conf<server><host>:%conf<server><port>"; + + react { + whenever signal(SIGINT) { + say "Shutting down..."; + $http.stop; + done; + } + } +} diff --git a/lib/Crater/Session.rakumod b/lib/Crater/Session.rakumod new file mode 100644 index 0000000..246c8bb --- /dev/null +++ b/lib/Crater/Session.rakumod @@ -0,0 +1,7 @@ +use Cro::HTTP::Auth; + +class Crater::Session does Cro::HTTP::Auth { + has Bool $.logged-in is rw; +} + +subset LoggedIn of Crater::Session is export where *.logged-in; diff --git a/resources/config.toml b/resources/config.toml new file mode 100644 index 0000000..1470751 --- /dev/null +++ b/resources/config.toml @@ -0,0 +1,3 @@ +[server] + host = "127.0.0.1" + port = 8000 diff --git a/resources/css/colors.css b/resources/css/colors.css new file mode 100644 index 0000000..6e180a5 --- /dev/null +++ b/resources/css/colors.css @@ -0,0 +1,215 @@ +/* + * Colors from Modus theme. + * https://gitlab.com/protesilaos/dotfiles/-/blob/master/emacs/.emacs.d/modus-themes/modus-themes.el + */ + +:root { + --bg-main: #ffffff; + --bg-dim: #f8f8f8; + --bg-alt: #f0f0f0; + + --bg-active: #d7d7d7; + --bg-inactive: #efefef; + + --bg-special-cold: #dde3f4; + --bg-special-mild: #c4ede0; + --bg-special-warm: #f0e0d4; + --bg-special-calm: #f8ddea; + + --bg-hl-alt-intense: #e8dfd1; + + --fg-main: #000000; + --fg-dim: #282828; + --fg-alt: #505050; + + --fg-active: #0a0a0a; + --fg-inactive: #404148; + + --fg-special-cold: #093060; + --fg-special-mild: #184034; + --fg-special-warm: #5d3026; + --fg-special-calm: #61284f; + + /* foregrounds that can be combined with bg-main, bg-dim, bg-alt */ + --red: #a60000; + --red-alt: #972500; + --red-alt-other: #a0132f; + --red-faint: #7f1010; + --red-alt-faint: #702f00; + --red-alt-other-faint: #7f002f; + + --green: #005e00; + --green-alt: #315b00; + --green-alt-other: #145c33; + --green-faint: #104410; + --green-alt-faint: #30440f; + --green-alt-other-faint: #0f443f; + + --yellow: #813e00; + --yellow-alt: #70480f; + --yellow-alt-other: #863927; + --yellow-faint: #5f4400; + --yellow-alt-faint: #5d5000; + --yellow-alt-other-faint: #5e3a20; + + --blue: #0031a9; + --blue-alt: #2544bb; + --blue-alt-other: #0000c0; + --blue-faint: #003497; + --blue-alt-faint: #0f3d8c; + --blue-alt-other-faint: #001087; + + --magenta: #721045; + --magenta-alt: #8f0075; + --magenta-alt-other: #5317ac; + --magenta-faint: #752f50; + --magenta-alt-faint: #7b206f; + --magenta-alt-other-faint: #55348e; + + --cyan: #00538b; + --cyan-alt: #30517f; + --cyan-alt-other: #005a5f; + --cyan-faint: #005077; + --cyan-alt-faint: #354f6f; + --cyan-alt-other-faint: #125458; + + /* combine with bg-main */ + --red-intense: #b60000; + --orange-intense: #904200; + --green-intense: #006800; + --yellow-intense: #605b00; + --blue-intense: #1f1fce; + --magenta-intense: #a8007f; + --purple-intense: #7f10d0; + --cyan-intense: #005f88; + + /* combine with bg-active, bg-inactive */ + --red-active: #8a0000; + --green-active: #004c2e; + --yellow-active: #702f00; + --blue-active: #0030b4; + --magenta-active: #5c2092; + --cyan-active: #003f8a; + + /* subtle goes with fg-dim. intense with fg-main. */ + --red-subtle-bg: #f2b0a2; + --red-intense-bg: #ff9f9f; + --green-subtle-bg: #aecf90; + --green-intense-bg: #5ada88; + --yellow-subtle-bg: #e4c340; + --yellow-intense-bg: #f5df23; + --blue-subtle-bg: #b5d0ff; + --blue-intense-bg: #77baff; + --magenta-subtle-bg: #f0d3ff; + --magenta-intense-bg: #d5baff; + --cyan-subtle-bg: #c0efff; + --cyan-intense-bg: #42cbd4; + + --yellow-nuanced-fg: #3f3000; +} +@media (prefers-color-scheme: dark) { + :root { + --bg-main: #000000; + --bg-dim: #100f10; + --bg-alt: #191a1b; + + --bg-active: #323232; + --bg-inactive: #1e1e1e; + + --bg-special-cold: #203448; + --bg-special-mild: #00322e; + --bg-special-warm: #382f27; + --bg-special-calm: #392a48; + + --bg-hl-alt-intense: #282e46; + + --fg-main: #ffffff; + --fg-dim: #e0e6f0; + --fg-alt: #a8a8a8; + + --fg-active: #f4f4f4; + --fg-inactive: #bfc0c4; + + --fg-special-cold: #c6eaff; + --fg-special-mild: #bfebe0; + --fg-special-warm: #f8dec0; + --fg-special-calm: #fbd6f4; + + /* foregrounds that can be combined with bg-main, bg-dim, bg-alt */ + --red: #ff8059; + --red-alt: #ef8b50; + --red-alt-other: #ff9077; + --red-faint: #ffa0a0; + --red-alt-faint: #f5aa80; + --red-alt-other-faint: #ff9fbf; + + --green: #44bc44; + --green-alt: #70b900; + --green-alt-other: #00c06f; + --green-faint: #78bf78; + --green-alt-faint: #99b56f; + --green-alt-other-faint: #88bf99; + + --yellow: #d0bc00; + --yellow-alt: #c0c530; + --yellow-alt-other: #d3b55f; + --yellow-faint: #d2b580; + --yellow-alt-faint: #cabf77; + --yellow-alt-other-faint: #d0ba95; + + --blue: #2fafff; + --blue-alt: #79a8ff; + --blue-alt-other: #00bcff; + --blue-faint: #82b0ec; + --blue-alt-faint: #a0acef; + --blue-alt-other-faint: #80b2f0; + + --magenta: #feacd0; + --magenta-alt: #f78fe7; + --magenta-alt-other: #b6a0ff; + --magenta-faint: #e0b2d6; + --magenta-alt-faint: #ef9fe4; + --magenta-alt-other-faint: #cfa6ff; + + --cyan: #00d3d0; + --cyan-alt: #4ae2f0; + --cyan-alt-other: #6ae4b9; + --cyan-faint: #90c4ed; + --cyan-alt-faint: #a0bfdf; + --cyan-alt-other-faint: #a4d0bb; + + /* combine with bg-main */ + --red-intense: #fe6060; + --orange-intense: #fba849; + --green-intense: #4fe42f; + --yellow-intense: #f0dd60; + --blue-intense: #4fafff; + --magenta-intense: #ff62d4; + --purple-intense: #9f80ff; + --cyan-intense: #3fdfd0; + + /* combine with bg-active, bg-inactive */ + --red-active: #ffa7ba; + --green-active: #70d73f; + --yellow-active: #dbbe5f; + --blue-active: #34cfff; + --magenta-active: #d5b1ff; + --cyan-active: #00d8b4; + + /* subtle goes with fg-dim. intense with fg-main. */ + --red-subtle-bg: #762422; + --red-intense-bg: #a4202a; + --green-subtle-bg: #2f4a00; + --green-intense-bg: #006800; + --yellow-subtle-bg: #604200; + --yellow-intense-bg: #874900; + --blue-subtle-bg: #10387c; + --blue-intense-bg: #2a40b8; + --magenta-subtle-bg: #49366e; + --magenta-intense-bg: #7042a2; + --cyan-subtle-bg: #00415e; + --cyan-intense-bg: #005f88; + + --yellow-nuanced-fg: #dfdfb0; + } +} diff --git a/resources/css/style.css b/resources/css/style.css new file mode 100644 index 0000000..a7ab098 --- /dev/null +++ b/resources/css/style.css @@ -0,0 +1,104 @@ +@import 'colors.css'; + +::selection { + background-color: var(--bg-hl-alt-intense); +} + +*, *:before, *:after { + box-sizing: border-box; +} + +body { + color: var(--fg-main); + background-color: var(--bg-main); + font-family: "Iosevka Aile", sans-serif; + + margin: 2em auto; + max-width: 90%; + line-height: 1.5; +} + +h1 { color: var(--fg); } +h2 { color: var(--fg-special-warm); } +h3 { color: var(--fg-special-cold); } +h4 { color: var(--fg-special-mild); } +h5 { color: var(--fg-special-calm); } +h6 { color: var(--yellow-nuanced-fg); } + +h1, h2, h3, h4, h5, h6, .title { + font-family: "Iosevka Etoile", serif; +} + +hr { color: var(--fg-alt); } + +a { color: var(--blue-alt-other); } +a:hover, a:focus { + color: var(--fg-dim); + background-color: var(--blue-subtle-bg); +} +a:visited { color: var(--cyan); } +a:visited:hover, a:visited:focus { + color: var(--fg-dim); + background-color: var(--cyan-subtle-bg); +} + +img { + display: block; + max-width: 100%; + box-shadow: var(--bg-inactive) 0px 0px 0px 1px, + var(--fg-inactive) 0px 0px 0px 1px inset; +} + +input, .alert { + color: var(--fg-main); + background-color: var(--bg-main); + border: 1px var(--bg-active) solid; + padding: 0.5em; + margin: 0.4em; + min-width: 30%; +} + +img, .text, .heading { + margin: 3.2em 1em; +} + +.heading { + box-shadow: var(--blue-intense-bg) 0px 0px 0px 2px inset, + var(--bg-main) 10px -10px 0px -3px, + var(--green-subtle-bg) 10px -10px, + var(--bg-main) 20px -20px 0px -3px, + var(--yellow-intense-bg) 20px -20px, + var(--bg-main) 30px -30px 0px -3px, + var(--red-subtle-bg) 30px -30px, + var(--bg-main) 40px -40px 0px -3px, + var(--red-intense-bg) 40px -40px; + padding: 1em; +} + +.text { + box-shadow: var(--magenta-intense-bg) 0px 0px 0px 3px, + var(--green-subtle-bg) 0px 0px 0px 6px, + var(--yellow-intense-bg) 0px 0px 0px 9px, + var(--red-subtle-bg) 0px 0px 0px 12px, + var(--red-intense-bg) 0px 0px 0px 15px; + padding: 1em; +} + +.gallery { + column-count: auto; + column-width: 384px; +} +.gallery img { + transform-origin: center; + transform: perspective(800px) rotateY(2deg); + transition: 0.4s; +} +.gallery:hover img { opacity: 0.4; } +.gallery img:hover { + transform: perspective(800px) rotateY(0deg) scale(1.1); + box-shadow: var(--bg-active) 0px 20px 30px -10px; + opacity: 1; +} +.alert { + background-color: var(--red-subtle-bg); +} diff --git a/templates/base.crotmp b/templates/base.crotmp new file mode 100644 index 0000000..f200241 --- /dev/null +++ b/templates/base.crotmp @@ -0,0 +1,14 @@ +<:macro page($title)> +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title><$title> - Crater</title> + <link rel="stylesheet" href="/resources/css/style.css"> + </head> + <body> + <:body> + </body> +</html> +</:macro> diff --git a/templates/gallery.crotmp b/templates/gallery.crotmp new file mode 100644 index 0000000..a47804f --- /dev/null +++ b/templates/gallery.crotmp @@ -0,0 +1,22 @@ +<:use 'templates/base.crotmp'> +<|page(.title)> +<div class="gallery"> + <@gallery : $i> + + <?{ $i.<type> eq 'img' }> + <img src="<$i.<src>>"> + </?> + + <?{ $i.<type> eq 'text' }> + <div class="text"> + <$i.<text>> + </div> + </?> + + <?{ $i.<type> eq 'heading' }> + <h1 class="heading"><$i.<text>></h1> + </?> + + </@> +</div> +</|> diff --git a/templates/login.crotmp b/templates/login.crotmp new file mode 100644 index 0000000..3dbee24 --- /dev/null +++ b/templates/login.crotmp @@ -0,0 +1,15 @@ +<:use 'templates/base.crotmp'> +<|page('Log In')> +<form method="post" action="/login"> + <?.error> + <div class="alert" role="alert"> + <.error> + </div> + </?> + + <input type="pass" name="pass" id="pass" placeholder="Password" required> + <br> + <input type="submit" value="Log In" /> +</form> +</div> +</|> |