feat: featured works on the /work landingpage and moved timeline (#16)

Reviewed-on: https://codeberg.org/vlw/vlw.se/pulls/16
This commit is contained in:
Victor Westerlund 2025-01-28 14:45:00 +00:00
parent ff7d4f5397
commit 3b51458dd4
41 changed files with 716 additions and 357 deletions

View file

@ -39,10 +39,7 @@
->min(1) ->min(1)
->max(parent::MYSQL_TEXT_MAX_LENGTH), ->max(parent::MYSQL_TEXT_MAX_LENGTH),
(new Rules(WorkModel::IS_LISTABLE->value)) (new Rules(WorkModel::IS_LISTED->value))
->type(Type::BOOLEAN),
(new Rules(WorkModel::IS_READABLE->value))
->type(Type::BOOLEAN), ->type(Type::BOOLEAN),
(new Rules(WorkModel::DATE_MODIFIED->value)) (new Rules(WorkModel::DATE_MODIFIED->value))

View file

@ -15,6 +15,8 @@
require_once Path::root("src/databases/VLWdb.php"); require_once Path::root("src/databases/VLWdb.php");
require_once Path::root("src/databases/models/Work/Work.php"); require_once Path::root("src/databases/models/Work/Work.php");
const PARAM_LIMIT = "limit";
class GET_Work extends VLWdb { class GET_Work extends VLWdb {
protected Ruleset $ruleset; protected Ruleset $ruleset;
@ -35,11 +37,7 @@
->type(Type::STRING) ->type(Type::STRING)
->max(parent::MYSQL_TEXT_MAX_LENGTH), ->max(parent::MYSQL_TEXT_MAX_LENGTH),
(new Rules(WorkModel::IS_LISTABLE->value)) (new Rules(WorkModel::IS_LISTED->value))
->type(Type::BOOLEAN)
->default(true),
(new Rules(WorkModel::IS_READABLE->value))
->type(Type::BOOLEAN) ->type(Type::BOOLEAN)
->default(true), ->default(true),
@ -51,15 +49,26 @@
(new Rules(WorkModel::DATE_CREATED->value)) (new Rules(WorkModel::DATE_CREATED->value))
->type(Type::NUMBER) ->type(Type::NUMBER)
->min(1) ->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH),
(new Rules(PARAM_LIMIT))
->type(Type::NUMBER)
->type(Type::NULL)
->min(1)
->max(parent::MYSQL_INT_MAX_LENGTH) ->max(parent::MYSQL_INT_MAX_LENGTH)
->default(null)
]); ]);
parent::__construct(Databases::VLW, $this->ruleset); parent::__construct(Databases::VLW, $this->ruleset);
} }
public function main(): Response { public function main(): Response {
// Use copy of search paramters as filters // Use search parameters from model as filters
$filters = $_GET; $filters = $_GET;
// Unset keys not included in database model from filter
foreach (array_diff(array_keys($_GET), WorkModel::values()) as $k) {
unset($filters[$k]);
}
// Do a wildcard search on the title column if provided // Do a wildcard search on the title column if provided
if (array_key_exists(WorkModel::TITLE->value, $_GET)) { if (array_key_exists(WorkModel::TITLE->value, $_GET)) {
@ -78,12 +87,12 @@
$response = $this->db->for(WorkModel::TABLE) $response = $this->db->for(WorkModel::TABLE)
->where($filters) ->where($filters)
->order([WorkModel::DATE_CREATED->value => "DESC"]) ->order([WorkModel::DATE_CREATED->value => "DESC"])
->limit($_GET[PARAM_LIMIT])
->select([ ->select([
WorkModel::ID->value, WorkModel::ID->value,
WorkModel::TITLE->value, WorkModel::TITLE->value,
WorkModel::SUMMARY->value, WorkModel::SUMMARY->value,
WorkModel::IS_LISTABLE->value, WorkModel::IS_LISTED->value,
WorkModel::IS_READABLE->value,
WorkModel::DATE_YEAR->value, WorkModel::DATE_YEAR->value,
WorkModel::DATE_MONTH->value, WorkModel::DATE_MONTH->value,
WorkModel::DATE_DAY->value, WorkModel::DATE_DAY->value,

View file

@ -47,10 +47,7 @@
->min(1) ->min(1)
->max(parent::MYSQL_TEXT_MAX_LENGTH), ->max(parent::MYSQL_TEXT_MAX_LENGTH),
(new Rules(WorkModel::IS_LISTABLE->value)) (new Rules(WorkModel::IS_LISTED->value))
->type(Type::BOOLEAN),
(new Rules(WorkModel::IS_READABLE->value))
->type(Type::BOOLEAN), ->type(Type::BOOLEAN),
(new Rules(WorkModel::DATE_MODIFIED->value)) (new Rules(WorkModel::DATE_MODIFIED->value))

View file

@ -41,11 +41,7 @@
->max(parent::MYSQL_TEXT_MAX_LENGTH) ->max(parent::MYSQL_TEXT_MAX_LENGTH)
->default(null), ->default(null),
(new Rules(WorkModel::IS_LISTABLE->value)) (new Rules(WorkModel::IS_LISTED->value))
->type(Type::BOOLEAN)
->default(false),
(new Rules(WorkModel::IS_READABLE->value))
->type(Type::BOOLEAN) ->type(Type::BOOLEAN)
->default(false), ->default(false),

View file

@ -38,12 +38,11 @@
WorkActionsModel::REF_WORK_ID->value, WorkActionsModel::REF_WORK_ID->value,
WorkActionsModel::DISPLAY_TEXT->value, WorkActionsModel::DISPLAY_TEXT->value,
WorkActionsModel::HREF->value, WorkActionsModel::HREF->value,
WorkActionsModel::CLASS_LIST->value, WorkActionsModel::CLASS_LIST->value
WorkActionsModel::EXTERNAL->value
]); ]);
return $response->num_rows > 0 return $response->num_rows > 0
? new Response($response->fetch_all(MYSQLI_ASSOC)) ? new Response(parent::index_array_by_key($response->fetch_all(MYSQLI_ASSOC), WorkActionsModel::REF_WORK_ID->value))
: new Response([], 404); : new Response([], 404);
} }
} }

View file

@ -50,11 +50,7 @@
(new Rules(WorkActionsModel::CLASS_LIST->value)) (new Rules(WorkActionsModel::CLASS_LIST->value))
->type(Type::ARRAY) ->type(Type::ARRAY)
->min(1) ->min(1)
->default([]), ->default([])
(new Rules(WorkActionsModel::EXTERNAL->value))
->type(Type::BOOLEAN)
->default(false)
]); ]);
parent::__construct(Databases::VLW, $this->ruleset); parent::__construct(Databases::VLW, $this->ruleset);

View file

@ -45,7 +45,7 @@
]); ]);
return $response->num_rows > 0 return $response->num_rows > 0
? new Response($response->fetch_all(MYSQLI_ASSOC)) ? new Response(parent::index_array_by_key($response->fetch_all(MYSQLI_ASSOC), WorkTagsModel::REF_WORK_ID->value))
: new Response([], 404); : new Response([], 404);
} }
} }

View file

@ -37,6 +37,11 @@
); );
} }
// Bail out if provided ReflectRules\Ruleset is invalid
private static function eval_ruleset_or_exit(Ruleset $ruleset): ?Response {
return !$ruleset->is_valid() ? new Response($ruleset->get_errors(), 422) : null;
}
// Generate and return UUID4 string // Generate and return UUID4 string
public static function gen_uuid4(): string { public static function gen_uuid4(): string {
return sprintf("%04x%04x-%04x-%04x-%04x-%04x%04x%04x", return sprintf("%04x%04x-%04x-%04x-%04x-%04x%04x%04x",
@ -76,8 +81,21 @@
return $filters; return $filters;
} }
// Bail out if provided ReflectRules\Ruleset is invalid public function index_array_by_key(array $input, string $key): array {
private static function eval_ruleset_or_exit(Ruleset $ruleset): ?Response { $output = [];
return !$ruleset->is_valid() ? new Response($ruleset->get_errors(), 422) : null;
foreach ($input as $item) {
$idx = $item[$key];
// Create entry for key in output array if first item
if (!array_key_exists($idx, $output)) {
$output[$idx] = [];
}
// Append item to array of array by key
$output[$idx][] = $item;
}
return $output;
} }
} }

View file

@ -2,18 +2,21 @@
namespace VLW\API\Databases\VLWdb\Models\Work; namespace VLW\API\Databases\VLWdb\Models\Work;
use victorwesterlund\xEnum;
enum WorkModel: string { enum WorkModel: string {
use xEnum;
const TABLE = "work"; const TABLE = "work";
case ID = "id"; case ID = "id";
case TITLE = "title"; case TITLE = "title";
case SUMMARY = "summary"; case SUMMARY = "summary";
case COVER_SRCSET = "cover_srcset"; case COVER_SRCSET = "cover_srcset";
case IS_LISTABLE = "is_listable"; case IS_LISTED = "is_listed";
case IS_READABLE = "is_readable"; case DATE_YEAR = "date_year";
case DATE_YEAR = "date_year"; case DATE_MONTH = "date_month";
case DATE_MONTH = "date_month"; case DATE_DAY = "date_day";
case DATE_DAY = "date_day"; case DATE_MODIFIED = "date_modified";
case DATE_MODIFIED = "date_modified"; case DATE_CREATED = "date_created";
case DATE_CREATED = "date_created";
} }

View file

@ -6,8 +6,10 @@
const TABLE = "work_actions"; const TABLE = "work_actions";
case REF_WORK_ID = "ref_work_id"; case REF_WORK_ID = "ref_work_id";
case ICON_PREFIX = "icon_prefix";
case ICON_SUFFIX = "icon_suffix";
case ORDER_IDX = "order_idx";
case DISPLAY_TEXT = "display_text"; case DISPLAY_TEXT = "display_text";
case HREF = "href"; case HREF = "href";
case CLASS_LIST = "class_list"; case CLASS_LIST = "class_list";
case EXTERNAL = "external";
} }

View file

@ -0,0 +1,15 @@
<?php
namespace VLW\API\Databases\VLWdb\Models\Work;
enum WorkNamespacesModel: string {
const TABLE = "work_actions";
case REF_WORK_ID = "ref_work_id";
case ICON_PREFIX = "icon_prefix";
case ICON_SUFFIX = "icon_suffix";
case ORDER_IDX = "order_idx";
case DISPLAY_TEXT = "display_text";
case HREF = "href";
case CLASS_LIST = "class_list";
}

View file

@ -10,6 +10,7 @@
case VLW; case VLW;
case RELEASE; case RELEASE;
case WEBSITE; case WEBSITE;
case REPO;
} }
enum WorkTagsModel: string { enum WorkTagsModel: string {

View file

@ -3,6 +3,9 @@
:root { :root {
--primer-color-accent: 3, 255, 219; --primer-color-accent: 3, 255, 219;
--color-accent: rgb(var(--primer-color-accent)); --color-accent: rgb(var(--primer-color-accent));
--color-reflect: 220, 26, 0;
--color-vegvisir: 0, 128, 255;
} }
vv-shell { vv-shell {
@ -16,179 +19,134 @@ vv-shell {
/* # Sections */ /* # Sections */
/* ## Git */ /* ## Hero */
section.git { section.hero {
display: flex; --color-accent: rgba(255, 255, 255);
flex-direction: column;
display: grid;
gap: var(--padding); gap: var(--padding);
background-color: rgba(var(--primer-color-accent), .1); grid-template-columns: repeat(1, 1fr);
padding: calc(var(--padding) * 1.5); }
section.hero .item {
width: 100%;
position: relative;
border-radius: 6px; border-radius: 6px;
} }
section.git svg { section.hero .wrapper {
fill: white;
width: 60px;
}
section.git .buttons {
display: flex;
flex-direction: column;
gap: var(--padding); gap: var(--padding);
} z-index: 1;
height: 100%;
/* ## Timeline */
section.timeline {
--timestamp-gap: calc(var(--padding) / 2);
width: 100%;
}
section.timeline :is(.year, .month, .day) {
display: grid;
grid-template-columns: calc(40px + var(--timestamp-gap)) 1fr;
grid-template-rows: 1fr;
}
section.timeline .track {
--opacity: .15;
--width: 2%;
background: linear-gradient(90deg,
transparent 0%, transparent calc(50% - var(--width)),
rgba(255, 255, 255, var(--opacity)) calc(50% - var(--width)), rgba(255, 255, 255, var(--opacity)) calc(50% + var(--width)),
transparent calc(50% + var(--width)), transparent 100%
);
}
section.timeline .track p {
position: sticky;
top: calc(var(--running-size) + var(--padding));
padding: calc(var(--padding) / 2) 0;
background-color: black;
color: var(--color-accent);
}
section.timeline :not(.year) > .track p::before {
content: "/ ";
color: rgba(255, 255, 255, .3);
}
/* ### Item */
section.timeline .items .item {
display: flex; display: flex;
position: relative;
align-items: baseline;
flex-direction: column; flex-direction: column;
gap: calc(var(--padding) / 2); padding: calc(var(--padding) * 1.5);
padding: var(--padding);
} }
section.timeline .items .item + .item { section.hero .item .title {
border-top: solid 2px rgba(255, 255, 255, .2); display: grid;
align-items: center;
gap: var(--padding);
grid-template-columns: 40px 1fr;
} }
section.timeline .items .item:first-of-type { section.hero .item .title svg {
margin-top: var(--padding); height: 3em;
border-top: solid 2px var(--color-accent);
}
/* No border style for the latest item (from the top) in the list */
section.timeline .year:first-of-type .month:first-of-type .day:first-of-type .items .item:first-of-type {
margin-top: unset;
border-top: unset;
}
section.timeline .items .item .tags {
display: flex;
gap: calc(var(--padding) / 2);
}
section.timeline .items .item .tags .tag {
font-size: 11px;
letter-spacing: 1px;
color: rgba(255, 255, 255, .7);
background-color: rgba(255, 255, 255, .15);
border-radius: 4px; border-radius: 4px;
padding: 5px 10px;
} }
section.timeline .items .item img { section.hero .actions {
max-width: 100%; margin-top: auto;
height: 250px;
} }
section.timeline .items .item .actions { /* ### Vegivisr */
margin-top: 7px;
section.hero .item.vegvisir {
--color-accent: var(--color-vegvisir);
color: rgb(var(--color-vegvisir));
background-color: rgba(var(--color-vegvisir), .1);
} }
/* ## Note */ /* ### Reflect */
section.note { section.hero .item.reflect {
text-align: center; --color-accent: var(--color-reflect);
color: rgb(var(--color-reflect));
background-color: rgba(var(--color-reflect), .2);
}
/* ## Heading */
section.heading {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
section.heading svg {
fill: white;
height: 2em;
}
/* ## Featured */
section.featured {
display: grid;
gap: var(--padding);
grid-template-columns: repeat(1, 1fr);
}
section.featured featured-item {
display: flex;
fill: white;
color: white;
border-radius: 8px;
align-items: baseline;
flex-direction: column;
padding: var(--padding);
background-color: rgba(255, 255, 255, .1);
}
section.featured featured-item .title {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: calc(var(--padding) / 2);
}
section.featured featured-item .title svg {
height: 2em;
fill: var(--color-accent);
}
/* ### Languages */
/* ### Actions */
section.featured featured-item .actions {
gap: 5px;
display: flex;
padding-top: var(--padding);
margin-top: auto;
} }
/* # Size queries */ /* # Size queries */
@media (min-width: 460px) { @media (min-width: 600px) {
section.git .buttons { section.hero {
flex-direction: row; grid-template-columns: repeat(2, 1fr);
} }
} }
@media (min-width: 900px) { @media (min-width: 900px) {
section.git { section.featured {
display: grid; grid-template-columns: repeat(3, 1fr);
grid-template-columns: 70px 1fr 400px;
align-items: center;
gap: calc(var(--padding) * 1.5);
}
section.git svg {
width: 100%;
}
section.git .buttons {
justify-content: end;
}
}
@media (max-width: 500px) {
section.timeline {
padding: unset;
}
section.timeline .track {
position: relative;
background: unset;
z-index: 10;
pointer-events: none;
}
section.timeline .track p {
background-color: black;
}
section.timeline :is(.years, .year, .months, .month, .days, .day) {
width: 0;
}
section.timeline .items {
position: relative;
left: -140px;
}
section.timeline .items .item {
padding: calc(var(--padding) * 1.5) 0;
width: calc(100vw - (var(--padding) * 3.5));
}
section.timeline .items .item:first-of-type {
border-top-color: rgba(var(--primer-color-accent), .2);
}
section.timeline .year:first-of-type .month:first-of-type .day:first-of-type .items .item:first-of-type {
margin-top: var(--padding);
} }
} }

View file

@ -0,0 +1,188 @@
/* # Overrides */
:root {
--primer-color-accent: 3, 255, 219;
--color-accent: rgb(var(--primer-color-accent));
}
vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
width: 100%;
max-width: 1200px;
overflow-x: initial;
}
/* # Sections */
/* ## Git */
section.git {
display: flex;
flex-direction: column;
gap: var(--padding);
background-color: rgba(var(--primer-color-accent), .1);
padding: calc(var(--padding) * 1.5);
border-radius: 6px;
}
section.git svg {
fill: white;
width: 60px;
}
section.git .buttons {
display: flex;
flex-direction: column;
gap: var(--padding);
}
/* ## Timeline */
section.timeline {
--timestamp-gap: calc(var(--padding) / 2);
width: 100%;
}
section.timeline :is(.year, .month, .day) {
display: grid;
grid-template-columns: calc(40px + var(--timestamp-gap)) 1fr;
grid-template-rows: 1fr;
}
section.timeline .track {
--opacity: .15;
--width: 2%;
background: linear-gradient(90deg,
transparent 0%, transparent calc(50% - var(--width)),
rgba(255, 255, 255, var(--opacity)) calc(50% - var(--width)), rgba(255, 255, 255, var(--opacity)) calc(50% + var(--width)),
transparent calc(50% + var(--width)), transparent 100%
);
}
section.timeline .track p {
position: sticky;
top: calc(var(--running-size) + var(--padding));
padding: calc(var(--padding) / 2) 0;
background-color: black;
color: var(--color-accent);
}
section.timeline :not(.year) > .track p::before {
content: "/ ";
color: rgba(255, 255, 255, .3);
}
/* ### Item */
section.timeline .items .item {
display: flex;
flex-direction: column;
gap: calc(var(--padding) / 2);
padding: var(--padding);
}
section.timeline .items .item + .item {
border-top: solid 2px rgba(255, 255, 255, .2);
}
section.timeline .items .item:first-of-type {
margin-top: var(--padding);
border-top: solid 2px var(--color-accent);
}
/* No border style for the latest item (from the top) in the list */
section.timeline .year:first-of-type .month:first-of-type .day:first-of-type .items .item:first-of-type {
margin-top: unset;
border-top: unset;
}
section.timeline .items .item .tags {
display: flex;
gap: calc(var(--padding) / 2);
}
section.timeline .items .item .tags .tag {
font-size: 11px;
letter-spacing: 1px;
color: rgba(255, 255, 255, .7);
background-color: rgba(255, 255, 255, .15);
border-radius: 4px;
padding: 5px 10px;
}
section.timeline .items .item img {
max-width: 100%;
height: 250px;
}
section.timeline .items .item .actions {
margin-top: 7px;
}
/* # Size queries */
@media (min-width: 460px) {
section.git .buttons {
flex-direction: row;
}
}
@media (max-width: 500px) {
section.timeline {
padding: unset;
}
section.timeline .track {
position: relative;
background: unset;
z-index: 10;
pointer-events: none;
}
section.timeline .track p {
background-color: black;
}
section.timeline :is(.years, .year, .months, .month, .days, .day) {
width: 0;
}
section.timeline .items {
position: relative;
left: -140px;
}
section.timeline .items .item {
padding: calc(var(--padding) * 1.5) 0;
width: calc(100vw - (var(--padding) * 3.5));
}
section.timeline .items .item:first-of-type {
border-top-color: rgba(var(--primer-color-accent), .2);
}
section.timeline .year:first-of-type .month:first-of-type .day:first-of-type .items .item:first-of-type {
margin-top: var(--padding);
}
}
@media (min-width: 900px) {
section.git {
display: grid;
grid-template-columns: 70px 1fr 400px;
align-items: center;
gap: calc(var(--padding) * 1.5);
}
section.git svg {
width: 100%;
}
section.git .buttons {
justify-content: end;
}
}

View file

@ -0,0 +1,15 @@
/* # Overrides */
:root {
--primer-color-accent: 3, 255, 219;
--color-accent: rgb(var(--primer-color-accent));
}
vv-shell {
display: flex;
flex-direction: column;
gap: var(--padding);
width: 100%;
max-width: 1200px;
overflow-x: initial;
}

View file

@ -285,6 +285,7 @@ search-results {
transform: scale(.99); transform: scale(.99);
transform-origin: 100% 0; transform-origin: 100% 0;
overflow-y: scroll; overflow-y: scroll;
z-index: 50;
} }
search-results:not([vv-page]) { search-results:not([vv-page]) {

View file

@ -0,0 +1 @@
<svg viewBox="0 0 60.965 60.965" xmlns="http://www.w3.org/2000/svg"><path style="fill:#dc1a00;fill-opacity:1;stroke-width:.529167" d="M0 0h135.467v135.467H0z" transform="matrix(.45004 0 0 .45004 0 0)"/><g style="fill:#fff;fill-opacity:1"><g fill="none" style="fill:#fff;fill-opacity:1"><path class="solid" d="M12 22 0 0h24z" style="display:inline;fill:#fff;fill-opacity:1" transform="matrix(.95357 0 0 .95357 19.04 10.01)"/><path class="stroke" d="M12 17.823 20.63 2H3.37L12 17.823M12 22 0 0h24z" style="display:inline;fill:#fff;fill-opacity:1" transform="matrix(.95357 0 0 .95357 19.04 10.01)"/></g><g opacity=".5" style="fill:#fff;fill-opacity:1"><path class="solid" d="M24 22 12 0h24z" style="display:inline;fill:#fff;fill-opacity:1" transform="matrix(-.95357 0 0 -.95357 53.368 51.968)"/><path class="stroke" d="M24 17.823 32.63 2H15.37L24 17.823M24 22 12 0h24z" style="display:inline;fill:#fff;fill-opacity:1" transform="matrix(-.95357 0 0 -.95357 53.368 51.968)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 984 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1 @@
<svg viewBox="0 0 60.965 60.965" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path style="fill:#0080ff;fill-opacity:1;stroke-width:.529167" d="M0 0h135.467v135.467H0z" transform="matrix(.45004 0 0 .45004 0 0)"/><g style="fill:#87ffff;fill-opacity:1"><g fill="none" style="fill:#87ffff;fill-opacity:1"><path class="solid" d="M12 22 0 0h24z" style="display:inline;fill:#87ffff;fill-opacity:1" transform="matrix(-.95357 0 0 -.95357 41.925 30.482)"/><path class="stroke" d="M12 17.823 20.63 2H3.37L12 17.823M12 22 0 0h24z" style="display:inline;fill:#87ffff;fill-opacity:1" transform="matrix(-.95357 0 0 -.95357 41.925 30.482)"/></g><g fill="none" style="fill:#87ffff;fill-opacity:1"><path class="solid" d="M12 22 0 0h24z" style="display:inline;fill:#87ffff;fill-opacity:1" transform="matrix(.95357 0 0 .95357 19.04 30.482)"/><path class="stroke" d="M12 17.823 20.63 2H3.37L12 17.823M12 22 0 0h24z" style="display:inline;fill:#87ffff;fill-opacity:1" transform="matrix(.95357 0 0 .95357 19.04 30.482)"/></g><g opacity=".5" style="fill:#87ffff;fill-opacity:1"><path class="solid" d="M24 22 12 0h24z" style="display:inline;fill:#87ffff;fill-opacity:1" transform="matrix(0 .95357 -.95357 0 29.529 7.597)"/><path class="stroke" d="M24 17.823 32.63 2H15.37L24 17.823M24 22 12 0h24z" style="display:inline;fill:#87ffff;fill-opacity:1" transform="matrix(0 .95357 -.95357 0 29.529 7.597)"/></g><g opacity=".5" style="fill:#87ffff;fill-opacity:1"><path class="solid" d="M24 22 12 0h24z" style="display:inline;fill:#87ffff;fill-opacity:1" transform="matrix(0 -.95357 .95357 0 30.482 53.368)"/><path class="stroke" d="M24 17.823 32.63 2H15.37L24 17.823M24 22 12 0h24z" style="display:inline;fill:#87ffff;fill-opacity:1" transform="matrix(0 -.95357 .95357 0 30.482 53.368)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

View file

@ -66,11 +66,7 @@
<?php foreach ($actions->json() as $action): ?> <?php foreach ($actions->json() as $action): ?>
<?php // Bind VV Interactions if link is same origin, else open in new tab ?> <?php // Bind VV Interactions if link is same origin, else open in new tab ?>
<?php if (!$action[WorkActionsModel::EXTERNAL->value]): ?> <a href="<?= $action[WorkActionsModel::HREF->value] ?>" target="_blank"><button class="inline <?= $action[WorkActionsModel::CLASS_LIST->value] ?>"><?= $action[WorkActionsModel::DISPLAY_TEXT->value] ?></button></a>
<a href="<?= $action[WorkActionsModel::HREF->value] ?>"><button class="inline <?= $action[WorkActionsModel::CLASS_LIST->value] ?>"><?= $action[WorkActionsModel::DISPLAY_TEXT->value] ?></button></a>
<?php else: ?>
<a href="<?= $action[WorkActionsModel::HREF->value] ?>" target="_blank"><button class="inline <?= $action[WorkActionsModel::CLASS_LIST->value] ?>"><?= $action[WorkActionsModel::DISPLAY_TEXT->value] ?></button></a>
<?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>

View file

@ -1,181 +1,138 @@
<?php <?php
use Vegvisir\Path; use Reflect\Response;
use VLW\Client\API; use VLW\Client\API;
use VLW\API\Endpoints; use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\Models\Work\{ use VLW\API\Databases\VLWdb\Models\Work\WorkModel;
WorkModel,
WorkTagsModel,
WorkActionsModel
};
require_once VV::root("src/client/API.php"); require_once VV::root("src/client/API.php");
require_once VV::root("api/src/Endpoints.php"); require_once VV::root("api/src/Endpoints.php");
require_once VV::root("api/src/databases/models/Work/Work.php"); require_once VV::root("api/src/databases/models/Work/Work.php");
require_once VV::root("api/src/databases/models/Work/WorkTags.php");
require_once VV::root("api/src/databases/models/Work/WorkActions.php");
// Connect to VLW API // Number of items from the timeline to display on this page
$api = new API(); const TIMELINE_PREVIEW_LIMIT = 5;
// Retreive rows from work endpoints $work = new class extends API {
$resp_work = $api->call(Endpoints::WORK->value)->get(); const ERROR_MSG = "Something went wrong";
// Resolve tags and actions if we got work results private readonly Response $resp;
if ($resp_work->ok) {
$work_tags = $api->call(Endpoints::WORK_TAGS->value)->get()->json(); public function __construct() {
$work_actions = $api->call(Endpoints::WORK_ACTIONS->value)->get()->json(); parent::__construct();
// Get work items from endpoint
$this->resp = $this->call(Endpoints::WORK->value)->params([
WorkModel::IS_LISTED->value => true
])->get();
}
private function get_item(string $key): array {
$idx = array_search($key, array_column($this->resp->json(), WorkModel::ID->value));
return $this->resp->json()[$idx];
}
public function get_summary(string $key): string {
return $this->resp->ok ? $this->get_item($key)[WorkModel::SUMMARY->value] : self::ERROR_MSG;
}
} }
?> ?>
<style><?= VV::css("public/assets/css/pages/work") ?></style> <style><?= VV::css("public/assets/css/pages/work") ?></style>
<section class="hero">
<section class="git"> <div class="item vegvisir">
<?= VV::embed("public/assets/media/icons/codeberg.svg") ?> <div class="wrapper">
<p>I have moved most of my free open-source software <a href="https://giveupgithub.com">away from GitHub</a> to <a href="https://codeberg.org/vlw">Codeberg</a>. I also have a mirror of everything and sources for some smaller projects on <a href="https://git.vlw.se">Forgejo</a>.</p> <div class="title">
<div class="buttons"> <?= VV::embed("public/assets/media/icons/vegvisir.svg") ?>
<a href="https://codeberg.org/vlw"><button class="inline solid">Codeberg</button></a> <h1>vegvisir</h1>
<a href="https://git.vlw.se"><button class="inline">Forgejo</button></a> </div>
<p><?= $work->get_summary("vlw/vegvisir") ?></p>
<div class="actions">
<a href="https://vegvisir.vlw.se"><button class="inline">read more</button></a>
</div>
</div>
</div>
<div class="item reflect">
<div class="wrapper">
<div class="title">
<?= VV::embed("public/assets/media/icons/reflect.svg") ?>
<h1>reflect</h1>
</div>
<p><?= $work->get_summary("vlw/reflect") ?></p>
<div class="actions">
<a href="https://reflect.vlw.se"><button class="inline">read more</button></a>
</div>
</div>
</div> </div>
</section> </section>
<section class="featured">
<?php if ($resp_work->ok): ?> <featured-item>
<?php <div class="title">
<?= VV::embed("public/assets/media/icons/vw.svg") ?>
/* </div>
Order response from endpoint into a multi-dimensional array. <h3>vlw.se</h3>
For example, a single item created at 14th of February 2024 would be ordered like this <p>Can I put my own website here, is that cheating? Maybe, but I think this site counts as the most important thing I've personally created. I've only used my own libraries and frameworks to create this website, so it kind of works as a live demonstration of many of my web projects bundled together.</p>
[2024 => [[02 => [14 => [<row_data>]]]]] <div class="actions">
*/ <a href="https://codeberg.org/vlw/vlw.se"><button class="inline">view source</button></a>
</div>
$rows = []; </featured-item>
// Create array of arrays ordered by decending year, month, day, items <featured-item>
foreach ($resp_work->json() as $row) { <div class="title">
// Create array for current year if it doesn't exist <?= VV::embed("public/assets/media/icons/vw.svg") ?>
if (!array_key_exists($row[WorkModel::DATE_YEAR->value], $rows)) { </div>
$rows[$row[WorkModel::DATE_YEAR->value]] = []; <h3>Silly dabbles</h3>
} <p>I create silly things for fun to challenge myself sometimes, and putting them all on the timeline is not right. So I made an appropriately-themed and named page to highlight most of my "what if I could" projects.</p>
<div class="actions">
// Create array for current month if it doesn't exist <a href="/playground"><button class="inline">playground</button></a>
if (!array_key_exists($row[WorkModel::DATE_MONTH->value], $rows[$row[WorkModel::DATE_YEAR->value]])) { </div>
$rows[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]] = []; </featured-item>
} <featured-item>
<div class="title">
// Create array for current day if it doesn't exist <?= VV::embed("public/assets/media/icons/repo.svg") ?>
if (!array_key_exists($row[WorkModel::DATE_DAY->value], $rows[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]])) { </div>
$rows[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]][$row[WorkModel::DATE_DAY->value]] = []; <h3>vlw/php-mysql</h3>
} <p><?= $work->get_summary("vlw/php-mysql") ?></p>
<div class="actions">
// Append item to ordered array <a href="https://codeberg.org/vlw/php-mysql"><button class="inline">view source</button></a>
$rows[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]][$row[WorkModel::DATE_DAY->value]][] = $row; </div>
} </featured-item>
<featured-item>
?> <div class="title">
<?= VV::embed("public/assets/media/icons/star.svg") ?>
<section class="timeline"> </div>
<?php // Get year int from key and array of months for current year ?> <h3>Website for iCellate Medical</h3>
<?php foreach($rows as $year => $months): ?> <p><?= $work->get_summary("icellate/website") ?></p>
<div class="year"> <div class="actions">
<div class="track"> <a href="/work/icellate/website"><button class="inline">read more</button></a>
<p><?= $year ?></p> </div>
</div> </featured-item>
<featured-item>
<div class="months"> <div class="title">
<?php // Get month int from key and array of days for current month ?> <?= VV::embed("public/assets/media/icons/star.svg") ?>
<?php foreach($months as $month => $days): ?> </div>
<div class="month"> <h3>Modernizing GeneMate by iCellate</h3>
<div class="track"> <p><?= $work->get_summary("icellate/genemate") ?></p>
<?php // Append leading zero to month ?> <div class="actions">
<p><?= sprintf("%02d", $month) ?></p> <a href="/work/icellate/genemate"><button class="inline">read more</button></a>
</div> </div>
</featured-item>
<div class="days"> <featured-item>
<?php // Get day int from key and array of items for current day ?> <div class="title">
<?php foreach($days as $day => $items): ?> <?= VV::embed("public/assets/media/icons/star.svg") ?>
<div class="day"> </div>
<div class="track"> <h3>Custom pages for Deltaco AB</h3>
<?php // Append leading zero to day ?> <p><?= $work->get_summary("deltaco/asyncapp") ?></p>
<p><?= sprintf("%02d", $day) ?></p> <div class="actions">
</div> <a href="/work/deltaco/asyncapp"><button class="inline">read more</button></a>
</div>
<div class="items"> </featured-item>
<?php foreach($items as $item): ?> </section>
<div class="item"> <section class="heading">
<h1>latest projects</h1>
<?php // Get array index ids from tags array where work entity id matches ref_work_id ?> </section>
<?php $tag_ids = array_keys(array_column($work_tags, WorkTagsModel::REF_WORK_ID->value), $item[WorkModel::ID->value]); ?> <?= VV::include("public/work/timeline?limit=" . TIMELINE_PREVIEW_LIMIT) ?>
<section class="heading">
<?php // List tags if available ?> <a href="/work/timeline"><button class="inline solid">view full timeline</button></a>
<?php if($tag_ids): ?> </section>
<div class="tags">
<?php foreach($tag_ids as $tag_id): ?>
<?php // Get tag details from tag array by index id ?>
<?php $tag = $work_tags[$tag_id]; ?>
<p class="tag <?= $tag[WorkTagsModel::NAME->value] ?>"><?= $tag[WorkTagsModel::NAME->value] ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php // Show large heading if defined ?>
<?php if (!empty($item[WorkModel::TITLE->value])): ?>
<h2><?= $item[WorkModel::TITLE->value] ?></h2>
<?php endif; ?>
<p><?= $item[WorkModel::SUMMARY->value] ?></p>
<?php // Get array index ids from actions array where work entity id matches ref_work_id ?>
<?php $action_ids = array_keys(array_column($work_actions, WorkTagsModel::REF_WORK_ID->value), $item[WorkModel::ID->value]); ?>
<?php // List actions if defined for item ?>
<?php if($action_ids): ?>
<div class="actions">
<?php foreach($action_ids as $action_id): ?>
<?php
// Get tag details from tag array by index id
$action = $work_actions[$action_id];
$link_attr = !$action[WorkActionsModel::EXTERNAL->value]
// Bind VV Interactions for local links
? "vv='work' vv-call='navigate'"
// Open external links in a new tab
: "target='_blank'";
$link_href = $action[WorkActionsModel::HREF->value] === null
// Navigate to work details page if no href is defined
? "/work/{$item[WorkModel::ID->value]}"
// Href is defined so use it directly
: $action[WorkActionsModel::HREF->value];
?>
<a href="<?= $link_href ?>" <?= $link_attr ?>><button class="inline <?= $action["class_list"] ?>"><?= $action["display_text"] ?></button></a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</section>
<section class="note">
<p>This is not really the end of the list. I will add some of my notable older work at some point.</p>
</section>
<?php else: ?>
<p>Something went wrong!</p>
<?php endif; ?>
<script><?= VV::js("assets/js/pages/work") ?></script>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

1
public/work/itg/lan.php Normal file
View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

184
public/work/timeline.php Normal file
View file

@ -0,0 +1,184 @@
<?php
use Vegvisir\Path;
use Reflect\Response;
use VLW\Client\API;
use VLW\API\Endpoints;
use VLW\API\Databases\VLWdb\Models\Work\{
WorkModel,
WorkTagsModel,
WorkActionsModel
};
require_once VV::root("src/client/API.php");
require_once VV::root("api/src/Endpoints.php");
require_once VV::root("api/src/databases/models/Work/Work.php");
require_once VV::root("api/src/databases/models/Work/WorkTags.php");
require_once VV::root("api/src/databases/models/Work/WorkActions.php");
$work = new class extends API {
private const API_PARAM_LIMIT = "limit";
private readonly Response $resp;
private readonly Response $tags;
private readonly Response $actions;
public function __construct() {
parent::__construct();
$this->resp = $this->call(Endpoints::WORK->value)->params([
WorkModel::IS_LISTED->value => true,
self::API_PARAM_LIMIT => $_GET[self::API_PARAM_LIMIT] ?? null
])->get();
// Fetch metadata for work items if we got an ok from work endpoint
if ($this->resp->ok) {
$this->tags = $this->call(Endpoints::WORK_TAGS->value)->get();
$this->actions = $this->call(Endpoints::WORK_ACTIONS->value)->get();
}
}
/*
Order response from endpoint into a multi-dimensional array.
For example, a single item created at 14th of February 2024 would be ordered like this
[2024 => [[02 => [14 => [<row_data>]]]]]
*/
public function get_timeline(): array {
if (!$this->resp->ok) {
return [];
}
$timeline = [];
// Create array of arrays ordered by decending year, month, day, items
foreach ($this->resp->json() as $row) {
// Create array for current year if it doesn't exist
if (!array_key_exists($row[WorkModel::DATE_YEAR->value], $timeline)) {
$timeline[$row[WorkModel::DATE_YEAR->value]] = [];
}
// Create array for current month if it doesn't exist
if (!array_key_exists($row[WorkModel::DATE_MONTH->value], $timeline[$row[WorkModel::DATE_YEAR->value]])) {
$timeline[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]] = [];
}
// Create array for current day if it doesn't exist
if (!array_key_exists($row[WorkModel::DATE_DAY->value], $timeline[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]])) {
$timeline[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]][$row[WorkModel::DATE_DAY->value]] = [];
}
// Append item to ordered array
$timeline[$row[WorkModel::DATE_YEAR->value]][$row[WorkModel::DATE_MONTH->value]][$row[WorkModel::DATE_DAY->value]][] = $row;
}
return $timeline;
}
public function get_tags(string $key): array {
if (!$this->resp->ok) {
return [];
}
return in_array($key, $this->tags->json()) ? $this->tags->json()[$key] : [];
}
public function get_actions(string $key): array {
if (!$this->resp->ok) {
return [];
}
return array_key_exists($key, $this->actions->json()) ? $this->actions->json()[$key] : [];
}
}
?>
<style><?= VV::css("public/assets/css/pages/work/timeline") ?></style>
<section class="git">
<?= VV::embed("public/assets/media/icons/codeberg.svg") ?>
<p>This timeline has most but not all of my FOSS software. If you want to see a list of all things I've created for the free software world, check out my repos on Codeberg or Forgejo.</p>
<div class="buttons">
<a href="https://codeberg.org/vlw"><button class="inline solid">Codeberg</button></a>
<a href="https://git.vlw.se"><button class="inline">Forgejo</button></a>
</div>
</section>
<section class="timeline">
<?php // Get year int from key and array of months for current year ?>
<?php foreach ($work->get_timeline() as $year => $months): ?>
<div class="year">
<div class="track">
<p><?= $year ?></p>
</div>
<div class="months">
<?php // Get month int from key and array of days for current month ?>
<?php foreach ($months as $month => $days): ?>
<div class="month">
<div class="track">
<?php // Append leading zero to month ?>
<p><?= sprintf("%02d", $month) ?></p>
</div>
<div class="days">
<?php // Get day int from key and array of items for current day ?>
<?php foreach ($days as $day => $items): ?>
<div class="day">
<div class="track">
<?php // Append leading zero to day ?>
<p><?= sprintf("%02d", $day) ?></p>
</div>
<div class="items">
<?php foreach ($items as $item): ?>
<div class="item">
<?php // List tags if available ?>
<?php if ($work->get_tags($item[WorkModel::ID->value])): ?>
<div class="tags">
<?php foreach ($work->get_tags($item[WorkModel::ID->value]) as $tag): ?>
<p class="tag <?= $tag[WorkTagsModel::NAME->value] ?>"><?= $tag[WorkTagsModel::NAME->value] ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php // Show large heading if defined ?>
<?php if (!empty($item[WorkModel::TITLE->value])): ?>
<h2><?= $item[WorkModel::TITLE->value] ?></h2>
<?php endif; ?>
<p><?= $item[WorkModel::SUMMARY->value] ?></p>
<div class="actions">
<?php if ($work->get_actions($item[WorkModel::ID->value])): ?>
<?php // Display each action button ?>
<?php foreach ($work->get_actions($item[WorkModel::ID->value]) as $action): ?>
<a href="<?= $action[WorkActionsModel::HREF->value] ?>"><button class="inline <?= $action[WorkActionsModel::CLASS_LIST->value] ?>"><?= $action[WorkActionsModel::DISPLAY_TEXT->value] ?></button></a>
<?php endforeach; ?>
<?php // Display a link to namespaced page on vlw.se if no action is defined ?>
<?php else: ?>
<a href="<?= $item[WorkModel::ID->value] ?>"><button class="inline">read more</button></a>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</section>
<script><?= VV::js("assets/js/pages/work/timeline") ?></script>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

View file

@ -0,0 +1 @@
<?= VV::include("public/work/wip") ?>

8
public/work/wip.php Normal file
View file

@ -0,0 +1,8 @@
<style><?= VV::css("public/assets/css/pages/work/wip") ?></style>
<section class="disclaimer">
<h1>Soon, very soon!</h1>
<p>Bear with me as I cook up some texts about this project! Hopefully with some pictures too.</p>
</section>
<section class="actions">
<a href="/work"><button class="inline">to featured work</button></a>
</section>