mirror of
https://github.com/standardebooks/web.git
synced 2025-07-06 14:50:39 -04:00
Add projects index page, and more detail on placeholder pages
This commit is contained in:
parent
fe5bb8ed48
commit
c7a4e34e31
15 changed files with 211 additions and 59 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.,
|
||||
*
|
||||
|
|
|
@ -22,4 +22,7 @@ enum DateTimeFormat: string{
|
|||
|
||||
/** Like `1641426132`. */
|
||||
case UnixTimestamp = 'U';
|
||||
|
||||
/** Like Jan 5, 2024 */
|
||||
case ShortDate = 'M j, Y';
|
||||
}
|
||||
|
|
7
lib/Exceptions/ProjectNotFoundException.php
Normal file
7
lib/Exceptions/ProjectNotFoundException.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class ProjectNotFoundException extends AppException{
|
||||
/** @var string $message */
|
||||
protected $message = 'We couldn’t locate that project.';
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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]) ?>
|
||||
<? } ?>
|
||||
|
|
48
templates/ProjectsTable.php
Normal file
48
templates/ProjectsTable.php
Normal 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>
|
|
@ -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){ ?>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
39
www/projects/index.php
Normal 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() ?>
|
Loading…
Add table
Add a link
Reference in a new issue