summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.cro.yml15
-rw-r--r--.gitignore8
-rw-r--r--META6.json31
-rw-r--r--README.org12
-rw-r--r--bin/crater3
-rw-r--r--lib/Crater/Gallery.rakumod25
-rw-r--r--lib/Crater/Routes.rakumod24
-rw-r--r--lib/Crater/Routes/Auth.rakumod34
-rw-r--r--lib/Crater/Routes/Gallery.rakumod25
-rw-r--r--lib/Crater/Service.rakumod52
-rw-r--r--lib/Crater/Session.rakumod7
-rw-r--r--resources/config.toml3
-rw-r--r--resources/css/colors.css215
-rw-r--r--resources/css/style.css104
-rw-r--r--templates/base.crotmp14
-rw-r--r--templates/gallery.crotmp22
-rw-r--r--templates/login.crotmp15
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>
+</|>