Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

* [PR-22](https://github.com/itk-dev/itk-project-database/pull/22)
Use the ITK logo in the nav and login, tidy the dashboard header, and refresh
the login screen.
* [PR-19](https://github.com/itk-dev/itk-project-database/pull/19)
Auto-upload files with a progress bar and image preview, view images in an
in-page lightbox, and refresh the media field styling.
* [PR-18](https://github.com/itk-dev/itk-project-database/pull/18)
Rework the dashboard with an outstanding-work panel and a redesigned activity
feed, add help text to every graph, and fix the mascot's finish nudges.
* [PR-17](https://github.com/itk-dev/itk-project-database/pull/17)
Fix the mascot nudges that never appeared, nudge users to finish incomplete
contacts with a link to their edit page.
Expand Down
21 changes: 20 additions & 1 deletion assets/controllers/dashboard_viz_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export default class extends Controller {
"timeline",
];

static values = {
empty: String,
};

connect() {
this.reduce = window.matchMedia(
"(prefers-reduced-motion: reduce)",
Expand Down Expand Up @@ -221,6 +225,18 @@ export default class extends Controller {
buildHeatmap() {
const d = this.viz;
const el = this.heatmapTarget;

// The heatmap only makes sense once there are both rows (departments) and
// columns (areas) to cross; until then show a placeholder, not bare headers.
if (!d.areas.length || !d.departments.length) {
el.style.gridTemplateColumns = "";
el.classList.remove("heat--paused");
el.innerHTML = `<p class="heat-empty">${this.esc(this.emptyValue)}</p>`;
this.heatColHeads = [];
this.heatCells = [];
return;
}

el.style.gridTemplateColumns = `minmax(120px, 168px) repeat(${d.areas.length}, minmax(40px, 1fr))`;
if (!this.reduce) {
el.classList.add("heat--paused");
Expand Down Expand Up @@ -254,6 +270,9 @@ export default class extends Controller {

updateHeatmap(initial = false) {
const d = this.viz;
if (!d.areas.length || !d.departments.length) {
return;
}
let max = 1;
d.heatmap.forEach((row) =>
row.forEach((v) => {
Expand Down Expand Up @@ -308,7 +327,7 @@ export default class extends Controller {
el.innerHTML = "";
if (!this.viz.collaboration.length) {
el.innerHTML =
'<p class="collab-empty">Ingen tværgående temaer endnu — kategorisér initiativer for at finde sammenfald.</p>';
'<p class="collab-empty">Ingen tværgående områder endnu — kategorisér initiativer for at finde sammenfald.</p>';
return;
}
// Hold the entrance paused until the panel scrolls into view; once seen,
Expand Down
Binary file added assets/images/itk-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
146 changes: 128 additions & 18 deletions assets/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,12 @@ h3 {
}

.top-nav__brand {
font-size: var(--itk-text-base);
font-weight: 700;
color: var(--itk-ink);
display: inline-flex;
align-items: center;
gap: var(--itk-space-2);
font-size: var(--itk-text-base);
font-weight: 700;
color: var(--itk-ink);
letter-spacing: -0.01em;
}

Expand All @@ -143,19 +143,25 @@ h3 {
text-decoration: none;
}

.top-nav__brand-mark {
width: 28px;
height: 28px;
border-radius: var(--itk-radius-2);
background: linear-gradient(135deg, var(--itk-blue), var(--itk-cyan));
display: inline-block;
.top-nav__brand-logo {
height: 26px;
width: auto;
display: block;
}

.top-nav__menu {
display: flex;
align-items: center;
gap: var(--itk-space-1);
margin-left: var(--itk-space-4);
}

@media (min-width: 1520px) {
.top-nav__menu {
position: absolute;
top: 0;
left: calc((100vw - var(--itk-container)) / 2 + var(--itk-space-5));
height: 64px;
}
}

.top-nav__link {
Expand Down Expand Up @@ -471,13 +477,31 @@ h3 {
padding: var(--itk-space-5);
}

.card--stretch {
align-self: stretch;
display: flex;
flex-direction: column;
}

.feed-fill {
position: relative;
flex: 1;
min-height: 230px;
}

.feed-fill .feed-scroll {
position: absolute;
inset: 0;
height: auto;
}

.card__header {
padding: var(--itk-space-4) var(--itk-space-5);
border-bottom: 1px solid var(--itk-slate-100);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--itk-space-3);
flex-direction: column;
align-items: flex-start;
gap: var(--itk-space-1);
}

.card__title {
Expand Down Expand Up @@ -573,7 +597,8 @@ h3 {
.card__sub {
font-size: var(--itk-text-sm);
color: var(--itk-slate-500);
margin: 0 0 var(--itk-space-4);
margin: 0;
line-height: 1.4;
}

.feed-scroll {
Expand Down Expand Up @@ -609,6 +634,12 @@ h3 {
min-width: 560px;
}

.heat-empty {
margin: 0;
color: var(--itk-slate-500);
font-size: var(--itk-text-sm);
}

.heat__collabel {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -844,6 +875,75 @@ h3 {
color: var(--itk-slate-500);
}

a.recent-item {
color: inherit;
text-decoration: none;
transition: background-color 0.15s ease;
}

a.recent-item:hover {
background-color: color-mix(in srgb, var(--itk-blue) 9%, transparent);
}

.recent-item__cta {
display: none;
align-items: center;
gap: 4px;
font-size: var(--itk-text-xs);
font-weight: 600;
color: var(--itk-blue);
white-space: nowrap;
}

a.recent-item:hover .badge {
display: none;
}

a.recent-item:hover .recent-item__cta {
display: inline-flex;
}

.recent-item__body {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}

.recent-item__body .recent-item__title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.todo-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--itk-space-2);
height: 100%;
padding: var(--itk-space-5);
text-align: center;
}

.todo-empty__icon {
color: var(--itk-green);
line-height: 0;
}

.todo-empty__title {
font-weight: 600;
color: var(--itk-ink);
}

.todo-empty__hint {
max-width: 34ch;
margin: 0 0 var(--itk-space-2);
font-size: var(--itk-text-sm);
color: var(--itk-slate-500);
}

.table-wrap {
overflow-x: auto;
}
Expand Down Expand Up @@ -1677,15 +1777,25 @@ body.is-lightbox-open {
.auth__brand {
display: flex;
align-items: center;
justify-content: center;
gap: var(--itk-space-2);
font-weight: 700;
font-size: var(--itk-text-md);
margin-bottom: var(--itk-space-2);
font-weight: 700;
color: var(--itk-ink);
margin-bottom: var(--itk-space-6);
}

.auth__logo {
height: 40px;
width: auto;
display: block;
}

.auth__title {
font-size: var(--itk-text-xl);
margin-bottom: var(--itk-space-6);
font-size: var(--itk-text-base);
font-weight: 600;
text-align: center;
margin-bottom: var(--itk-space-3);
}

.auth .form-row {
Expand Down
8 changes: 7 additions & 1 deletion src/Controller/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace App\Controller;

use App\Entity\User;
use App\Repository\ContactRepository;
use App\Repository\InitiativeRepository;
use App\Service\DashboardData;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
Expand All @@ -13,11 +15,15 @@
class DashboardController extends AbstractController
{
#[Route('/', name: 'app_dashboard', methods: ['GET'])]
public function index(InitiativeRepository $initiatives, DashboardData $dashboardData): Response
public function index(InitiativeRepository $initiatives, ContactRepository $contacts, DashboardData $dashboardData): Response
{
$user = $this->getUser();

return $this->render('dashboard/index.html.twig', [
'recent' => $initiatives->findRecent(8),
'viz' => $dashboardData->build(),
'unfinishedInitiatives' => $user instanceof User ? $initiatives->findUnfinishedListByCreator($user) : [],
'incompleteContacts' => $user instanceof User ? $contacts->findIncompleteListByCreator($user) : [],
]);
}
}
18 changes: 18 additions & 0 deletions src/Entity/Initiative.php
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,24 @@ public function getCompletionPercentage(): int
return (int) round(\count(array_filter($checks)) / \count($checks) * 100);
}

/**
* Whether the initiative has been edited since it was created — drives the
* "updated" vs "created" label in the dashboard activity feed. Only counts as
* an edit once it's more than a day past creation, so the initial save and any
* same-day tweaks still read as "created".
*/
public function wasUpdatedAfterCreation(): bool
{
$created = $this->getCreatedAt();
$updated = $this->getUpdatedAt();

if (null === $created || null === $updated) {
return false;
}

return $updated->getTimestamp() - $created->getTimestamp() > 60 * 60 * 24;
}

public function __toString(): string
{
return (string) $this->title;
Expand Down
16 changes: 14 additions & 2 deletions src/Repository/ContactRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ public function findOrCreate(string $name): Contact
* Used by the mascot to nudge them to fill in the rest.
*/
public function findIncompleteByCreator(User $user): ?Contact
{
return $this->findIncompleteListByCreator($user, 1)[0] ?? null;
}

/**
* The user's contacts that still lack an email (created name-only and not yet
* finished), most recent first, capped at $limit. Surfaced on the dashboard
* as outstanding work.
*
* @return Contact[]
*/
public function findIncompleteListByCreator(User $user, int $limit = 6): array
{
// createdBy is a ManyToOne to the UserInterface (resolved to User via
// resolve_target_entities); binding the entity to a ULID FK doesn't match,
Expand All @@ -72,8 +84,8 @@ public function findIncompleteByCreator(User $user): ?Contact
->andWhere("(c.email IS NULL OR c.email = '')")
->setParameter('user', $user->getId(), 'ulid')
->orderBy('c.createdAt', 'DESC')
->setMaxResults(1)
->setMaxResults($limit)
->getQuery()
->getOneOrNullResult();
->getResult();
}
}
Loading
Loading