diff --git a/bin/compiled.world.psgi b/bin/compiled.world.psgi index 06b2f4b..68a1e4f 100644 --- a/bin/compiled.world.psgi +++ b/bin/compiled.world.psgi @@ -4,6 +4,7 @@ use 5.036; use strict; use warnings; +use DDP; use Plack::Builder; use Plack::Session::Store::DBI; @@ -28,11 +29,13 @@ my $conn = DBIx::Connector->new( $db_conf->get('password') ); -my $router = CW::Router->new(); -$router->get('/')->action('Index#index'); - CW::Migration->migrate( $conn->dbh ); +my $router = CW::Router->new(); + +$router->get('/')->action('Index#index'); +$router->get('/{board}')->action('Board#index'); + builder { enable 'Session', store => Plack::Session::Store::DBI->new( get_dbh => sub { diff --git a/cpanfile b/cpanfile index 1b2987a..4f71aa2 100644 --- a/cpanfile +++ b/cpanfile @@ -5,3 +5,5 @@ requires "Moo"; requires "Starman"; requires "Router::Simple"; requires "utf8::all"; +requires "Data::Printer"; +requires "Try::Tiny"; diff --git a/dev_db.sh b/dev_db.sh new file mode 100755 index 0000000..7071305 --- /dev/null +++ b/dev_db.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +podman run -p 3306:3306 -e MARIADB_ROOT_PASSWORD=compiledworld -e MARIADB_DATABASE=compiledworld -e MARIADB_USER=compiledworld -e MARIADB_PASSWORD=compiledworld -d docker.io/library/mariadb:11.8 diff --git a/lib/CW.pm b/lib/CW.pm index 9a0a1fe..d9b4fef 100644 --- a/lib/CW.pm +++ b/lib/CW.pm @@ -6,6 +6,9 @@ package CW; use Moo; use Plack::Request; +use Try::Tiny; + +use feature 'state'; use CW::Plack::Request; use CW::Plack::Response; @@ -47,11 +50,52 @@ sub app { my $route = $match->{route}; my $pkg = $route->pkg; my $action = $route->action; - $pkg->new( dbh => $self->dbh )->$action( $request, $response ); + + my %params = %$match; + delete $params{route}; + + try { + $pkg->new( + dbh => $self->dbh, + params => \%params, + request => $request, + response => $response + )->$action(); + } + catch { + my $error = $_; + + use DDP; + p $error; + + state $show_errors = $self->environment ne 'prod'; + if ( ref($error) && ref($error) eq 'HASH' && $error->{status} ) + { + $response->status( $error->{status} ); + $response->template( + "Error::" . $error->{status}, + { + show_errors => $show_errors, + error => $error + } + ); + } + else { + $response->status(500); + $response->template( + "Error::500", + { + show_errors => $show_errors, + error => $error + } + ); + } + } + } else { $response->status(404); - $response->body("404 not found"); + $response->template("Error::404"); } return $response->finalize; diff --git a/lib/CW/Controller.pm b/lib/CW/Controller.pm index 6466b11..d5d24ca 100644 --- a/lib/CW/Controller.pm +++ b/lib/CW/Controller.pm @@ -2,6 +2,21 @@ package CW::Controller; use Moo; -has dbh => ( is => 'ro', required => 1 ); +use Types::Standard qw(HashRef); + +has dbh => ( is => 'ro', required => 1 ); +has request => ( is => 'ro', required => 1 ); +has response => ( is => 'ro', required => 1 ); +has params => ( + is => 'ro', + isa => HashRef, + default => sub { return +{} } +); + +sub evac { + my ( $self, $code ) = @_; + $self->response->status($code); + $self->response->template("Error::$code"); +} 1; diff --git a/lib/CW/Controller/Board.pm b/lib/CW/Controller/Board.pm new file mode 100644 index 0000000..0b8ab79 --- /dev/null +++ b/lib/CW/Controller/Board.pm @@ -0,0 +1,59 @@ +package CW::Controller::Board; + +use Moo; + +extends 'CW::Controller'; + +has page_size => ( + is => 'lazy', + default => sub { 32 } +); + +sub index { + my ($self) = @_; + + my $board_identifier = $self->params->{board}; + + if ( !$board_identifier ) { + return $self->evac(404); + } + + my $board = + $self->dbh->selectrow_hashref( "SELECT * FROM boards WHERE id = ?", + undef, $board_identifier ); + + if ( !$board ) { + return $self->evac(404); + } + + my ($total_posts) = $self->dbh->selectrow_array( + "SELECT COUNT(id) FROM posts WHERE board_id = ? ORDER BY created_at", + undef, $board_identifier ); + + my $query = $self->request->query_parameters; + my $page = $query->{p} // 1; + my $page_size = $self->page_size; + my $offset = $page == 1 ? 0 : $page - 1; + + my $posts = $self->dbh->selectall_arrayref( + "SELECT * FROM posts WHERE board_id = ?" + . " ORDER BY created_at LIMIT ? , ?", + undef, + $board_identifier, + $page_size + 0, + $page_size * $offset + ); + + $self->response->template( + 'Board', + { + page => $page, + page_size => $page_size, + total_posts => $total_posts, + board => $board, + posts => $posts + } + ); +} + +1; diff --git a/lib/CW/Controller/Index.pm b/lib/CW/Controller/Index.pm index 0903852..d02b046 100644 --- a/lib/CW/Controller/Index.pm +++ b/lib/CW/Controller/Index.pm @@ -5,13 +5,13 @@ use Moo; extends 'CW::Controller'; sub index { - my ( $self, $request, $response ) = @_; + my ($self) = @_; my $boards = $self->dbh->selectall_arrayref( q[SELECT * FROM boards ORDER BY id], { Slice => {} } ); - $response->template( 'Index', + $self->response->template( 'Index', { title => 'compiled.world | home', boards => $boards } ); } diff --git a/lib/CW/Layout.pm b/lib/CW/Layout.pm index 5db8fed..0430144 100644 --- a/lib/CW/Layout.pm +++ b/lib/CW/Layout.pm @@ -6,7 +6,9 @@ use Types::Standard qw(HashRef InstanceOf); has template => ( is => 'ro', required => 1, - isa => InstanceOf ['CW::Template'] + isa => sub { + ref($_) && ref($_) =~ /^CW::Template/; + } ); has ctx => ( diff --git a/lib/CW/Layout/Default.pm b/lib/CW/Layout/Default.pm index fda0601..ddd09fc 100644 --- a/lib/CW/Layout/Default.pm +++ b/lib/CW/Layout/Default.pm @@ -14,7 +14,6 @@ sub render { if ( !$ctx->{nav} ) { $ctx->{nav} = []; my @path = split '/', $ctx->{env}->{PATH_INFO}; - if (@path) { my $nav = []; for ( 0 .. $#path ) { @@ -41,7 +40,10 @@ sub render { push @additional_links, $self->_make_nav_link("logout"); } - my $board = $ctx->{board} // 'cw'; + my $board = 'cw'; + if ( $ctx->{board} ) { + $board = $ctx->{board}->{id}; + } return [ head => [ @@ -68,18 +70,18 @@ sub render { rel => 'stylesheet' }, link => { - href => '/static/css/' . $board . '.css', + href => '/static/css/cw.css', rel => 'stylesheet' } ], body => [ - header => [ + header => { style => "margin-top: 1rem" } => [ div => [ div => { class => "header-wrapper" } => [ img => { - src => "/static/images/$board.svg", + src => "/static/images/cw.svg", class => "logo" }, div => [ @@ -108,18 +110,27 @@ sub render { ], ] ], + hr => { style => 'margin: 1rem; display: block;' }, main => [ - nav => { class => "quicknav" } => [ - map { - ( - li => [ - "/", - a => { href => $_->{path} } => [ $_->{text} ] - ] - ) - } @{ $ctx->{nav} } - ], - div => $self->template->render + ( + @{ $ctx->{nav} } + ? ( + nav => { class => "quicknav" } => [ + map { + ( + li => [ + "/", + a => { href => $_->{path} } => + [ $_->{text} ] + ] + ) + } @{ $ctx->{nav} } + ] + ) + : () + ), + div => { class => "main-content-wrapper" } => + $self->template->render ], footer => [] ] diff --git a/lib/CW/Migration/05.pm b/lib/CW/Migration/05.pm new file mode 100644 index 0000000..75d87b1 --- /dev/null +++ b/lib/CW/Migration/05.pm @@ -0,0 +1,26 @@ +package CW::Migration::05; + +use Moo; + +extends 'CW::Migration'; + +sub id { + return 5; +} + +sub sql { + return q[CREATE TABLE posts ( +id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, +board_id VARCHAR(3) NOT NULL, +user_id INT NOT NULL, +parent_id INT, +title VARCHAR(24), +image VARCHAR(44) NOT NULL, +body TEXT NOT NULL, +created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +) +] +} + +1; diff --git a/lib/CW/Plack/Request.pm b/lib/CW/Plack/Request.pm index b4a316d..83d3661 100644 --- a/lib/CW/Plack/Request.pm +++ b/lib/CW/Plack/Request.pm @@ -11,7 +11,7 @@ has _request => ( is => 'ro', init_arg => 'request', required => 1, - handles => [qw(env)] + handles => [qw(env query_parameters)] ); sub new_response { diff --git a/lib/CW/Router.pm b/lib/CW/Router.pm index a8d4ee8..4a7f720 100644 --- a/lib/CW/Router.pm +++ b/lib/CW/Router.pm @@ -15,6 +15,7 @@ has _router => ( { ## no critic [TestingAndDebugging::ProhibitNoStrict] no strict 'refs'; + for (qw(get post put patch delete)) { *{ __PACKAGE__ . '::' . $_ } = sub { my ( $self, $path ) = @_; @@ -23,6 +24,10 @@ has _router => ( my $route = CW::Route->new( path => $path ); $self->_router->connect( $path, { route => $route } ); + if ( $path !~ /.*\/$/ ) { + $self->_router->connect( "$path/", { route => $route } ); + } + return $route; } } diff --git a/lib/CW/Template/Board.pm b/lib/CW/Template/Board.pm new file mode 100644 index 0000000..2e63b91 --- /dev/null +++ b/lib/CW/Template/Board.pm @@ -0,0 +1,35 @@ +package CW::Template::Board; + +use Moo; + +extends 'CW::Template'; + +sub render { + my ($self) = @_; + + my $board_id = $self->ctx->{board}->{id}; + my $posts = $self->ctx->{posts}; + + return [ + div => { class => "board-catalog-wrapper" } => [ + map { + ( + div => { class => "board-catalog-post" } => [ + img => { + class => "board-catalog-post-img", + src => "/static/images/userdata/" . $_->{image} + }, + a => { href => "/" . $board_id . "/" . $_->{id} } => [ + h4 => { class => "board-catalog-post-title" } => + [ $_->{title} ] + ], + p => { class => "board-catalog-post-body" } => + [ $_->{body} ] + ] + ) + } @$posts + ] + ]; +} + +1; diff --git a/lib/CW/Template/Error/404.pm b/lib/CW/Template/Error/404.pm new file mode 100644 index 0000000..4da8c86 --- /dev/null +++ b/lib/CW/Template/Error/404.pm @@ -0,0 +1,20 @@ +package CW::Template::Error::404; + +use Moo; + +extends 'CW::Template'; + +sub render { + return [ + h2 => { style => "margin: 0" } => + [q[You are searching for something that seemingly doesn't exist.]], + div => [ + p => { style => "margin: 0" } => + ["Perhaps this is the sign you needed to create it yourself?"], + img => + { class => 'alexandria', src => "/static/images/alexandria.jpg" } + ] + ]; +} + +1; diff --git a/lib/CW/Template/Error/500.pm b/lib/CW/Template/Error/500.pm new file mode 100644 index 0000000..6cd4cbd --- /dev/null +++ b/lib/CW/Template/Error/500.pm @@ -0,0 +1,28 @@ +package CW::Template::Error::500; + +use Moo; + +extends 'CW::Template'; + +sub render { + my ($self) = @_; + return [ + h2 => { style => "margin: 0" } => + [q[Something has gone terribly wrong.]], + div => [ + p => { style => "margin: 0" } => + ["Please re-try whatever it is you were doing, later."], + img => { + class => 'alexandria', + src => "/static/images/alexandria_fire.jpg" + }, + ( + $self->ctx->{show_errors} + ? ( p => [ $self->ctx->{error} ] ) + : () + ) + ] + ]; +} + +1; diff --git a/lib/CW/Template/Index.pm b/lib/CW/Template/Index.pm index 66639a8..c0c258a 100644 --- a/lib/CW/Template/Index.pm +++ b/lib/CW/Template/Index.pm @@ -9,18 +9,32 @@ sub render { my @boards = @{ $self->ctx->{boards} }; return [ - map { - ( - li => [ - a => { - href => '/' . $_->{id} . '/', - class => 'index-board-link' - } => [ '/' . $_->{id} . '/ - ' . $_->{name} ], - p => { class => 'index-board-description' } => - [ $_->{description} ] - ] - ) - } @boards + div => { class => "index-content" } => [ + h2 => ["Boards"], + ul => { class => "board-list" } => [ + map { + ( + li => { class => "board-list-item" } => [ + a => { + href => '/' . $_->{id} . '/', + class => 'index-board-link' + } => [ '/' . $_->{id} . '/ - ' . $_->{name} ], + + # p => { + # style => "margin: 0", + # class => 'index-board-description' + # } => [ $_->{description} ] + ] + ) + } @boards + ] + ], + div => { class => "index-content" } => [ + h2 => ["Changelog"], + ul => { class => 'changelog-list' } => [ + li => ["Absofuckinglutely nothing"] + ] + ] ]; } diff --git a/public/static/css/a.css b/public/static/css/a.css new file mode 100644 index 0000000..9535449 --- /dev/null +++ b/public/static/css/a.css @@ -0,0 +1,9 @@ +* { + color: darkgreen; + text-decoration-color: darkgoldenrod; +} + +h1, h2, h3, h4, h5, p, a { + color: darkgreen; + text-decoration-color: darkgoldenrod; +} diff --git a/public/static/css/cw.css b/public/static/css/cw.css index ec6378a..06faf9c 100644 --- a/public/static/css/cw.css +++ b/public/static/css/cw.css @@ -1,3 +1,8 @@ +* { + color: darkgreen; + text-decoration-color: darkgreen; +} + h1, h2, h3, h4, h5, p, a { color: darkgreen; text-decoration-color: darkgreen; diff --git a/public/static/css/index.css b/public/static/css/index.css new file mode 100644 index 0000000..a17358c --- /dev/null +++ b/public/static/css/index.css @@ -0,0 +1,3 @@ +.index-content { + +} diff --git a/public/static/css/shared.css b/public/static/css/shared.css index 99c2f5a..25c9d65 100644 --- a/public/static/css/shared.css +++ b/public/static/css/shared.css @@ -17,8 +17,17 @@ h1 { font-size: 2.5rem; } +h2 { + font-size: 1.77rem; + margin-top: 0.33rem; +} + +h3 { + font-size: 1.33rem; +} + p, a { - font-size: calc(1.33rem + 0.01vh); + font-size: calc(1.11rem + 0.01vh); } /* Header */ @@ -32,6 +41,19 @@ p, a { flex-direction: row; } +/* Main */ +main { + margin-left: 1rem; + display: flex; + flex-direction: column; +} + +div.main-content-wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + /* Navigation */ .navbar-link { font-size: 1.2rem; @@ -45,6 +67,32 @@ nav.navbar { justify-content: space-between; } +nav.quicknav { + margin-top: 1rem; +} + +/* Error screens */ +img.alexandria { + width: 33rem; + height: 30.33.rem; + display: block; + margin-top: 1rem; +} + +/* Lists */ +ul { + list-style-type: "⛛ "; +} + +li { + margin-bottom: 0.33rem; +} + +/* Index page stuff */ +.index-content { + margin-right: 0.33rem; +} + /* Mobile */ @media screen and (max-width: 480px) { .header-wrapper { diff --git a/public/static/images/a.svg b/public/static/images/a.svg new file mode 100644 index 0000000..30d88ed --- /dev/null +++ b/public/static/images/a.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/static/images/alexandria.jpg b/public/static/images/alexandria.jpg new file mode 100644 index 0000000..f36db0a Binary files /dev/null and b/public/static/images/alexandria.jpg differ diff --git a/public/static/images/alexandria_fire.jpg b/public/static/images/alexandria_fire.jpg new file mode 100644 index 0000000..a4dca49 Binary files /dev/null and b/public/static/images/alexandria_fire.jpg differ