This commit is contained in:
2026-08-22 23:59:41 -06:00
parent 84893662ca
commit a89b275313
23 changed files with 374 additions and 39 deletions

View File

@@ -4,6 +4,7 @@ use 5.036;
use strict; use strict;
use warnings; use warnings;
use DDP;
use Plack::Builder; use Plack::Builder;
use Plack::Session::Store::DBI; use Plack::Session::Store::DBI;
@@ -28,11 +29,13 @@ my $conn = DBIx::Connector->new(
$db_conf->get('password') $db_conf->get('password')
); );
my $router = CW::Router->new();
$router->get('/')->action('Index#index');
CW::Migration->migrate( $conn->dbh ); CW::Migration->migrate( $conn->dbh );
my $router = CW::Router->new();
$router->get('/')->action('Index#index');
$router->get('/{board}')->action('Board#index');
builder { builder {
enable 'Session', store => Plack::Session::Store::DBI->new( enable 'Session', store => Plack::Session::Store::DBI->new(
get_dbh => sub { get_dbh => sub {

View File

@@ -5,3 +5,5 @@ requires "Moo";
requires "Starman"; requires "Starman";
requires "Router::Simple"; requires "Router::Simple";
requires "utf8::all"; requires "utf8::all";
requires "Data::Printer";
requires "Try::Tiny";

5
dev_db.sh Executable file
View File

@@ -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

View File

@@ -6,6 +6,9 @@ package CW;
use Moo; use Moo;
use Plack::Request; use Plack::Request;
use Try::Tiny;
use feature 'state';
use CW::Plack::Request; use CW::Plack::Request;
use CW::Plack::Response; use CW::Plack::Response;
@@ -47,11 +50,52 @@ sub app {
my $route = $match->{route}; my $route = $match->{route};
my $pkg = $route->pkg; my $pkg = $route->pkg;
my $action = $route->action; 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 { else {
$response->status(404); $response->status(404);
$response->body("404 not found"); $response->template("Error::404");
} }
return $response->finalize; return $response->finalize;

View File

@@ -2,6 +2,21 @@ package CW::Controller;
use Moo; 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; 1;

View File

@@ -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;

View File

@@ -5,13 +5,13 @@ use Moo;
extends 'CW::Controller'; extends 'CW::Controller';
sub index { sub index {
my ( $self, $request, $response ) = @_; my ($self) = @_;
my $boards = my $boards =
$self->dbh->selectall_arrayref( q[SELECT * FROM boards ORDER BY id], $self->dbh->selectall_arrayref( q[SELECT * FROM boards ORDER BY id],
{ Slice => {} } ); { Slice => {} } );
$response->template( 'Index', $self->response->template( 'Index',
{ title => 'compiled.world | home', boards => $boards } ); { title => 'compiled.world | home', boards => $boards } );
} }

View File

@@ -6,7 +6,9 @@ use Types::Standard qw(HashRef InstanceOf);
has template => ( has template => (
is => 'ro', is => 'ro',
required => 1, required => 1,
isa => InstanceOf ['CW::Template'] isa => sub {
ref($_) && ref($_) =~ /^CW::Template/;
}
); );
has ctx => ( has ctx => (

View File

@@ -14,7 +14,6 @@ sub render {
if ( !$ctx->{nav} ) { if ( !$ctx->{nav} ) {
$ctx->{nav} = []; $ctx->{nav} = [];
my @path = split '/', $ctx->{env}->{PATH_INFO}; my @path = split '/', $ctx->{env}->{PATH_INFO};
if (@path) { if (@path) {
my $nav = []; my $nav = [];
for ( 0 .. $#path ) { for ( 0 .. $#path ) {
@@ -41,7 +40,10 @@ sub render {
push @additional_links, $self->_make_nav_link("logout"); push @additional_links, $self->_make_nav_link("logout");
} }
my $board = $ctx->{board} // 'cw'; my $board = 'cw';
if ( $ctx->{board} ) {
$board = $ctx->{board}->{id};
}
return [ return [
head => [ head => [
@@ -68,18 +70,18 @@ sub render {
rel => 'stylesheet' rel => 'stylesheet'
}, },
link => { link => {
href => '/static/css/' . $board . '.css', href => '/static/css/cw.css',
rel => 'stylesheet' rel => 'stylesheet'
} }
], ],
body => [ body => [
header => [ header => { style => "margin-top: 1rem" } => [
div => [ div => [
div => { div => {
class => "header-wrapper" class => "header-wrapper"
} => [ } => [
img => { img => {
src => "/static/images/$board.svg", src => "/static/images/cw.svg",
class => "logo" class => "logo"
}, },
div => [ div => [
@@ -108,18 +110,27 @@ sub render {
], ],
] ]
], ],
hr => { style => 'margin: 1rem; display: block;' },
main => [ main => [
nav => { class => "quicknav" } => [ (
map { @{ $ctx->{nav} }
( ? (
li => [ nav => { class => "quicknav" } => [
"/", map {
a => { href => $_->{path} } => [ $_->{text} ] (
] li => [
) "/",
} @{ $ctx->{nav} } a => { href => $_->{path} } =>
], [ $_->{text} ]
div => $self->template->render ]
)
} @{ $ctx->{nav} }
]
)
: ()
),
div => { class => "main-content-wrapper" } =>
$self->template->render
], ],
footer => [] footer => []
] ]

26
lib/CW/Migration/05.pm Normal file
View File

@@ -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;

View File

@@ -11,7 +11,7 @@ has _request => (
is => 'ro', is => 'ro',
init_arg => 'request', init_arg => 'request',
required => 1, required => 1,
handles => [qw(env)] handles => [qw(env query_parameters)]
); );
sub new_response { sub new_response {

View File

@@ -15,6 +15,7 @@ has _router => (
{ {
## no critic [TestingAndDebugging::ProhibitNoStrict] ## no critic [TestingAndDebugging::ProhibitNoStrict]
no strict 'refs'; no strict 'refs';
for (qw(get post put patch delete)) { for (qw(get post put patch delete)) {
*{ __PACKAGE__ . '::' . $_ } = sub { *{ __PACKAGE__ . '::' . $_ } = sub {
my ( $self, $path ) = @_; my ( $self, $path ) = @_;
@@ -23,6 +24,10 @@ has _router => (
my $route = CW::Route->new( path => $path ); my $route = CW::Route->new( path => $path );
$self->_router->connect( $path, { route => $route } ); $self->_router->connect( $path, { route => $route } );
if ( $path !~ /.*\/$/ ) {
$self->_router->connect( "$path/", { route => $route } );
}
return $route; return $route;
} }
} }

35
lib/CW/Template/Board.pm Normal file
View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -9,18 +9,32 @@ sub render {
my @boards = @{ $self->ctx->{boards} }; my @boards = @{ $self->ctx->{boards} };
return [ return [
map { div => { class => "index-content" } => [
( h2 => ["Boards"],
li => [ ul => { class => "board-list" } => [
a => { map {
href => '/' . $_->{id} . '/', (
class => 'index-board-link' li => { class => "board-list-item" } => [
} => [ '/' . $_->{id} . '/ - ' . $_->{name} ], a => {
p => { class => 'index-board-description' } => href => '/' . $_->{id} . '/',
[ $_->{description} ] class => 'index-board-link'
] } => [ '/' . $_->{id} . '/ - ' . $_->{name} ],
)
} @boards # p => {
# style => "margin: 0",
# class => 'index-board-description'
# } => [ $_->{description} ]
]
)
} @boards
]
],
div => { class => "index-content" } => [
h2 => ["Changelog"],
ul => { class => 'changelog-list' } => [
li => ["Absofuckinglutely nothing"]
]
]
]; ];
} }

9
public/static/css/a.css Normal file
View File

@@ -0,0 +1,9 @@
* {
color: darkgreen;
text-decoration-color: darkgoldenrod;
}
h1, h2, h3, h4, h5, p, a {
color: darkgreen;
text-decoration-color: darkgoldenrod;
}

View File

@@ -1,3 +1,8 @@
* {
color: darkgreen;
text-decoration-color: darkgreen;
}
h1, h2, h3, h4, h5, p, a { h1, h2, h3, h4, h5, p, a {
color: darkgreen; color: darkgreen;
text-decoration-color: darkgreen; text-decoration-color: darkgreen;

View File

@@ -0,0 +1,3 @@
.index-content {
}

View File

@@ -17,8 +17,17 @@ h1 {
font-size: 2.5rem; font-size: 2.5rem;
} }
h2 {
font-size: 1.77rem;
margin-top: 0.33rem;
}
h3 {
font-size: 1.33rem;
}
p, a { p, a {
font-size: calc(1.33rem + 0.01vh); font-size: calc(1.11rem + 0.01vh);
} }
/* Header */ /* Header */
@@ -32,6 +41,19 @@ p, a {
flex-direction: row; 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 */ /* Navigation */
.navbar-link { .navbar-link {
font-size: 1.2rem; font-size: 1.2rem;
@@ -45,6 +67,32 @@ nav.navbar {
justify-content: space-between; 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 */ /* Mobile */
@media screen and (max-width: 480px) { @media screen and (max-width: 480px) {
.header-wrapper { .header-wrapper {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 KiB