Add projects index page, and more detail on placeholder pages

This commit is contained in:
Alex Cabal 2024-12-14 21:08:45 -06:00
parent fe5bb8ed48
commit c7a4e34e31
15 changed files with 211 additions and 59 deletions

View file

@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS `Benefits` (
`CanCreateEbookPlaceholders` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanCreateEbookPlaceholders` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanManageProjects` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanManageProjects` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanReviewProjects` tinyint(1) unsigned NOT NULL DEFAULT 0, `CanReviewProjects` tinyint(1) unsigned NOT NULL DEFAULT 0,
`CanEditProjects` tinyint(1) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`UserId`), PRIMARY KEY (`UserId`),
KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`) KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View file

@ -18,6 +18,9 @@ class Benefits{
public bool $CanEditCollections = false; public bool $CanEditCollections = false;
public bool $CanEditEbooks = false; public bool $CanEditEbooks = false;
public bool $CanCreateEbookPlaceholders = false; public bool $CanCreateEbookPlaceholders = false;
public bool $CanManageProjects = false;
public bool $CanReviewProjects = false;
public bool $CanEditProjects = false;
protected bool $_HasBenefits; protected bool $_HasBenefits;
@ -36,6 +39,12 @@ class Benefits{
$this->CanEditEbooks $this->CanEditEbooks
|| ||
$this->CanCreateEbookPlaceholders $this->CanCreateEbookPlaceholders
||
$this->CanManageProjects
||
$this->CanReviewProjects
||
$this->CanEditProjects
){ ){
return true; return true;
} }

View file

@ -1028,6 +1028,17 @@ class Ebook{
return $ebook; return $ebook;
} }
/**
* @throws Exceptions\EbookNotFoundException If the `Ebook` can't be found.
*/
public static function Get(?int $ebookId): Ebook{
if($ebookId === null){
throw new Exceptions\EbookNotFoundException();
}
return Db::Query('SELECT * from Ebooks where EbookId = ?', [$ebookId], Ebook::class)[0] ?? throw new Exceptions\EbookNotFoundException();
}
/** /**
* Joins the `Name` properites of `Contributor` objects as a URL slug, e.g., * Joins the `Name` properites of `Contributor` objects as a URL slug, e.g.,
* *

View file

@ -22,4 +22,7 @@ enum DateTimeFormat: string{
/** Like `1641426132`. */ /** Like `1641426132`. */
case UnixTimestamp = 'U'; case UnixTimestamp = 'U';
/** Like Jan 5, 2024 */
case ShortDate = 'M j, Y';
} }

View file

@ -0,0 +1,7 @@
<?
namespace Exceptions;
class ProjectNotFoundException extends AppException{
/** @var string $message */
protected $message = 'We couldnt locate that project.';
}

View file

@ -62,7 +62,7 @@ class Library{
// Add a period to the abbreviated month, but not if it's May (the only 3-letter month) // Add a period to the abbreviated month, but not if it's May (the only 3-letter month)
$obj->UpdatedString = preg_replace('/^(.+?)(?<!May) /', '\1. ', $obj->UpdatedString); $obj->UpdatedString = preg_replace('/^(.+?)(?<!May) /', '\1. ', $obj->UpdatedString);
if($obj->Updated->format('Y') != NOW->format('Y')){ if($obj->Updated->format('Y') != NOW->format('Y')){
$obj->UpdatedString = $obj->Updated->format('M j, Y'); $obj->UpdatedString = $obj->Updated->format(Enums\DateTimeFormat::ShortDate->value);
} }
// Sort the downloads by filename extension // Sort the downloads by filename extension

View file

@ -31,6 +31,11 @@ class Project{
protected User $_ReviewerUser; protected User $_ReviewerUser;
protected string $_Url; protected string $_Url;
// *******
// GETTERS
// *******
protected function GetUrl(): string{ protected function GetUrl(): string{
if(!isset($this->_Url)){ if(!isset($this->_Url)){
$this->_Url = '/projects/' . $this->ProjectId; $this->_Url = '/projects/' . $this->ProjectId;
@ -39,6 +44,11 @@ class Project{
return $this->_Url; return $this->_Url;
} }
// *******
// METHODS
// *******
/** /**
* @throws Exceptions\InvalidProjectException If the `Project` is invalid. * @throws Exceptions\InvalidProjectException If the `Project` is invalid.
*/ */
@ -218,4 +228,27 @@ class Project{
$this->PropertyFromHttp('ManagerUserId'); $this->PropertyFromHttp('ManagerUserId');
$this->PropertyFromHttp('ReviewerUserId'); $this->PropertyFromHttp('ReviewerUserId');
} }
// ***********
// ORM METHODS
// ***********
/**
* @throws Exceptions\ProjectNotFoundException If the `Project` can't be found.
*/
public static function Get(?int $projectId): Project{
if($projectId === null){
throw new Exceptions\ProjectNotFoundException();
}
return Db::Query('SELECT * from Projects where ProjectId = ?', [$projectId], Project::class)[0] ?? throw new Exceptions\ProjectNotFoundException();
}
/**
* @return array<Project>
*/
public static function GetAllByStatus(Enums\ProjectStatusType $status): array{
return Db::Query('SELECT * from Projects where Status = ? order by Started desc', [$status], Project::class);
}
} }

View file

@ -4,7 +4,7 @@
* @var array<stdClass> $collections * @var array<stdClass> $collections
*/ */
?> ?>
<table class="download-list"> <table class="data-table bulk-downloads-table">
<caption aria-hidden="hidden">Scroll right </caption> <caption aria-hidden="hidden">Scroll right </caption>
<thead> <thead>
<tr class="mid-header"> <tr class="mid-header">

View file

@ -10,10 +10,6 @@
<td>Ebook ID:</td> <td>Ebook ID:</td>
<td><?= $ebook->EbookId ?></td> <td><?= $ebook->EbookId ?></td>
</tr> </tr>
<tr>
<td>Identifier:</td>
<td><?= Formatter::EscapeHtml($ebook->Identifier) ?></td>
</tr>
<? if($ebook->IsPlaceholder() && $ebook->EbookPlaceholder !== null){ ?> <? if($ebook->IsPlaceholder() && $ebook->EbookPlaceholder !== null){ ?>
<tr> <tr>
<td>Is wanted:</td> <td>Is wanted:</td>
@ -27,24 +23,13 @@
<? } ?> <? } ?>
<tr> <tr>
<td>Difficulty:</td> <td>Difficulty:</td>
<td><?= ucfirst($ebook->EbookPlaceholder->Difficulty->value) ?></td> <td><?= ucfirst($ebook->EbookPlaceholder->Difficulty->value ?? '') ?></td>
</tr>
<? } ?>
<? if(sizeof($ebook->Projects) > 0){ ?>
<tr>
<td>Projects:</td>
<td>
<ul>
<? foreach($ebook->Projects as $project){ ?>
<li>
<p>
<?= $project->Started->format(Enums\DateTimeFormat::FullDateTime->value) ?> — <?= $project->Status->GetDisplayName() ?> — <? if($project->ProducerEmail !== null){ ?><a href="mailto:<?= Formatter::EscapeHtml($project->ProducerEmail) ?>"><?= Formatter::EscapeHtml($project->ProducerName) ?></a><? }else{ ?><?= Formatter::EscapeHtml($project->ProducerName) ?><? } ?> — <a href="<?= $project->Url ?>">Link</a>
</p>
</li>
<? } ?>
</ul>
</td>
</tr> </tr>
<? } ?> <? } ?>
</tbody> </tbody>
</table> </table>
<? if(sizeof($ebook->Projects) > 0){ ?>
<h2>Projects</h2>
<?= Template::ProjectsTable(['projects' => $ebook->Projects, 'includeTitle' => false]) ?>
<? } ?>

View file

@ -0,0 +1,48 @@
<?
/**
* @var array<Project> $projects
*/
$includeTitle = $includeTitle ?? true;
?>
<table class="data-table">
<caption aria-hidden="hidden">Scroll right </caption>
<thead>
<tr class="mid-header">
<? if($includeTitle){ ?>
<th scope="col">Title</th>
<? } ?>
<th scope="col">Started</th>
<th scope="col">Producer</th>
<th scope="col">Status</th>
<th/>
</tr>
</thead>
<tbody>
<? foreach($projects as $project){ ?>
<tr>
<? if($includeTitle){ ?>
<td class="row-header">
<a href="<?= $project->Ebook->Url ?>"><?= Formatter::EscapeHtml($project->Ebook->Title) ?></a>
</td>
<? } ?>
<td>
<?= $project->Started->format(Enums\DateTimeFormat::ShortDate->value) ?>
</td>
<td class="producer">
<? if($project->ProducerEmail !== null){ ?>
<a href="mailto:<?= Formatter::EscapeHtml($project->ProducerEmail) ?>"><?= Formatter::EscapeHtml($project->ProducerName) ?></a>
<? }else{ ?>
<?= Formatter::EscapeHtml($project->ProducerName) ?>
<? } ?>
</td>
<td class="status">
<?= ucfirst($project->Status->GetDisplayName()) ?>
</td>
<td>
<a href="<?= Formatter::EscapeHtml($project->VcsUrl) ?>">GitHub repo</a>
</td>
</tr>
<? } ?>
</tbody>
</table>

View file

@ -36,7 +36,7 @@ $title = preg_replace('/s$/', '', ucfirst($class));
<? } ?> <? } ?>
<p>These zip files contain each ebook in every format we offer, and are kept updated with the latest versions of each ebook. Read about <a href="/help/how-to-use-our-ebooks#which-file-to-download">which file format to download</a>.</p> <p>These zip files contain each ebook in every format we offer, and are kept updated with the latest versions of each ebook. Read about <a href="/help/how-to-use-our-ebooks#which-file-to-download">which file format to download</a>.</p>
<? if($class == 'months'){ ?> <? if($class == 'months'){ ?>
<table class="download-list"> <table class="data-table">
<caption aria-hidden="hidden">Scroll right </caption> <caption aria-hidden="hidden">Scroll right </caption>
<tbody> <tbody>
<? foreach($collection as $year => $months){ ?> <? foreach($collection as $year => $months){ ?>

View file

@ -731,90 +731,93 @@ ul.message.error > li + li{
text-align: center; text-align: center;
} }
.download-list{ .data-table{
margin: auto; margin: auto;
} }
.download-list caption{ .data-table caption{
font-style: italic; font-style: italic;
text-align: right; text-align: right;
display: none; display: none;
} }
.download-list .mid-header{ .data-table .mid-header{
font-style: italic; font-style: italic;
} }
.download-list thead tr.mid-header:first-child > *{ .data-table thead tr.mid-header:first-child > *{
padding-top: 2rem; padding-top: 2rem;
} }
.download-list th.row-header, .data-table td,
.download-list .mid-header th:first-child, .data-table th{
.download-list .mid-header th:last-child{
text-align: left;
}
.download-list td,
.download-list th{
padding: .25rem .5rem; padding: .25rem .5rem;
hyphens: none; hyphens: none;
white-space: nowrap; white-space: nowrap;
} }
.download-list th{ .data-table th{
font-weight: normal; font-weight: normal;
}
.data-table.bulk-downloads-table th.row-header,
.data-table.bulk-downloads-table .mid-header th:first-child,
.data-table.bulk-downloads-table .mid-header th:last-child{
text-align: left;
}
.data-table.bulk-downloads-table th{
text-align: right; text-align: right;
} }
.download-list .number{ .data-table .number{
text-align: right; text-align: right;
} }
.download-list td.download{ .data-table td.download{
padding-right: 0; padding-right: 0;
color: var(--body-text); color: var(--body-text);
} }
.download-list td.download + td{ .data-table td.download + td{
padding-left: .25rem; padding-left: .25rem;
font-size: .75em; font-size: .75em;
color: var(--sub-text); color: var(--sub-text);
} }
.download-list tbody .row-header{ .data-table tbody .row-header{
font-weight: bold; font-weight: bold;
text-align: left; text-align: left;
white-space: normal; white-space: normal;
} }
.download-list tbody tr td, .data-table tbody tr td,
.download-list tbody tr th{ .data-table tbody tr th{
border-top: 1px dashed var(--table-border); border-top: 1px dashed var(--table-border);
} }
.download-list tbody tr:first-child > *, .data-table tbody tr:first-child > *,
.download-list tbody tr.year-header > *, .data-table tbody tr.year-header > *,
.download-list tbody tr.year-header + tr > *, .data-table tbody tr.year-header + tr > *,
.download-list tbody tr.mid-header tr > *, .data-table tbody tr.mid-header tr > *,
.download-list tbody tr.mid-header + tr td, .data-table tbody tr.mid-header + tr td,
.download-list tbody tr.mid-header + tr th{ .data-table tbody tr.mid-header + tr th{
border: none; border: none;
} }
.download-list tbody tr:not([class]):hover > *{ .data-table tbody tr:not([class]):hover > *{
background: var(--table-row-hover); background: var(--table-row-hover);
} }
.download-list tbody tr:only-child:not([class]):hover > *{ .data-table tbody tr:only-child:not([class]):hover > *{
background: unset; /* Don't highlight on hover if there's only one row */ background: unset; /* Don't highlight on hover if there's only one row */
} }
h2 + .download-list tr.year-header:first-child th{ h2 + .data-table tr.year-header:first-child th{
padding-top: 2rem; padding-top: 2rem;
} }
.download-list .year-header th{ .data-table .year-header th{
padding-top: 4rem; padding-top: 4rem;
font-size: 1.4rem; font-size: 1.4rem;
font-family: "League Spartan", Arial, sans-serif; font-family: "League Spartan", Arial, sans-serif;
@ -3327,23 +3330,23 @@ table.admin-table td + td{
} }
@media(max-width: 1200px){ @media(max-width: 1200px){
.download-list{ .data-table{
overflow-x: scroll; overflow-x: scroll;
display: block; /* needed to make overflow work */ display: block; /* needed to make overflow work */
width: 100%; width: 100%;
} }
.download-list .year-header th{ .data-table .year-header th{
text-align: left; text-align: left;
} }
.download-list thead tr.mid-header:first-child th, .data-table thead tr.mid-header:first-child th,
.download-list tr.year-header:first-child th{ .data-table tr.year-header:first-child th{
padding-top: 0; padding-top: 0;
} }
.download-list caption{ .data-table caption{
display: block; display: block;
padding-top: 2rem; padding-top: 2rem;
} }

View file

@ -11,3 +11,16 @@
.project-form label:has(input[name="project-manager-user-id"]){ .project-form label:has(input[name="project-manager-user-id"]){
grid-column-start: 1; grid-column-start: 1;
} }
h2 + table.projects{
margin-top: 2rem;
}
table.projects thead{
font-style: italic;
}
table.data-table .status,
table.data-table .producer{
white-space: nowrap;
}

View file

@ -283,7 +283,7 @@ catch(Exceptions\EbookNotFoundException){
<ol> <ol>
<? foreach($ebook->GitCommits as $commit){ ?> <? foreach($ebook->GitCommits as $commit){ ?>
<li> <li>
<time datetime="<?= $commit->Created->format(DateTimeImmutable::RFC3339) ?>"><?= $commit->Created->format('M j, Y') ?></time> <time datetime="<?= $commit->Created->format(DateTimeImmutable::RFC3339) ?>"><?= $commit->Created->format(Enums\DateTimeFormat::ShortDate->value) ?></time>
<p> <p>
<a href="<?= Formatter::EscapeHtml($ebook->GitHubUrl) ?>/commit/<?= Formatter::EscapeHtml($commit->Hash) ?>"><?= Formatter::EscapeHtml($commit->Message) ?></a> <a href="<?= Formatter::EscapeHtml($ebook->GitHubUrl) ?>/commit/<?= Formatter::EscapeHtml($commit->Hash) ?>"><?= Formatter::EscapeHtml($commit->Message) ?></a>
</p> </p>

39
www/projects/index.php Normal file
View file

@ -0,0 +1,39 @@
<?
try{
if(Session::$User === null){
throw new Exceptions\LoginRequiredException();
}
if(!Session::$User->Benefits->CanEditProjects){
throw new Exceptions\InvalidPermissionsException();
}
$inProgressProjects = Project::GetAllByStatus(Enums\ProjectStatusType::InProgress);
$stalledProjects = Project::GetAllByStatus(Enums\ProjectStatusType::Stalled);
}
catch(Exceptions\LoginRequiredException){
Template::RedirectToLogin();
}
catch(Exceptions\InvalidPermissionsException){
Template::Emit403();
}
?><?= Template::Header(['title' => 'Projects', 'css' => ['/css/project.css'], 'description' => 'Ebook projects currently underway at Standard Ebooks.']) ?>
<main>
<section class="narrow">
<h1>Projects</h1>
<h2>Active projects</h2>
<? if(sizeof($inProgressProjects) == 0){ ?>
<p>
<i>None.</i>
</p>
<? }else{ ?>
<?= Template::ProjectsTable(['projects' => $inProgressProjects]) ?>
<? } ?>
<? if(sizeof($stalledProjects) > 0){ ?>
<h2>Stalled projects</h2>
<?= Template::ProjectsTable(['projects' => $stalledProjects]) ?>
<? } ?>
</section>
</main>
<?= Template::Footer() ?>