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,
`CanManageProjects` 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`),
KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View file

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

View file

@ -1028,6 +1028,17 @@ class 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.,
*

View file

@ -22,4 +22,7 @@ enum DateTimeFormat: string{
/** Like `1641426132`. */
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)
$obj->UpdatedString = preg_replace('/^(.+?)(?<!May) /', '\1. ', $obj->UpdatedString);
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

View file

@ -31,6 +31,11 @@ class Project{
protected User $_ReviewerUser;
protected string $_Url;
// *******
// GETTERS
// *******
protected function GetUrl(): string{
if(!isset($this->_Url)){
$this->_Url = '/projects/' . $this->ProjectId;
@ -39,6 +44,11 @@ class Project{
return $this->_Url;
}
// *******
// METHODS
// *******
/**
* @throws Exceptions\InvalidProjectException If the `Project` is invalid.
*/
@ -218,4 +228,27 @@ class Project{
$this->PropertyFromHttp('ManagerUserId');
$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
*/
?>
<table class="download-list">
<table class="data-table bulk-downloads-table">
<caption aria-hidden="hidden">Scroll right </caption>
<thead>
<tr class="mid-header">

View file

@ -10,10 +10,6 @@
<td>Ebook ID:</td>
<td><?= $ebook->EbookId ?></td>
</tr>
<tr>
<td>Identifier:</td>
<td><?= Formatter::EscapeHtml($ebook->Identifier) ?></td>
</tr>
<? if($ebook->IsPlaceholder() && $ebook->EbookPlaceholder !== null){ ?>
<tr>
<td>Is wanted:</td>
@ -27,24 +23,13 @@
<? } ?>
<tr>
<td>Difficulty:</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>
<td><?= ucfirst($ebook->EbookPlaceholder->Difficulty->value ?? '') ?></td>
</tr>
<? } ?>
</tbody>
</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>
<? if($class == 'months'){ ?>
<table class="download-list">
<table class="data-table">
<caption aria-hidden="hidden">Scroll right </caption>
<tbody>
<? foreach($collection as $year => $months){ ?>

View file

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

View file

@ -11,3 +11,16 @@
.project-form label:has(input[name="project-manager-user-id"]){
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>
<? foreach($ebook->GitCommits as $commit){ ?>
<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>
<a href="<?= Formatter::EscapeHtml($ebook->GitHubUrl) ?>/commit/<?= Formatter::EscapeHtml($commit->Hash) ?>"><?= Formatter::EscapeHtml($commit->Message) ?></a>
</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() ?>