Skip to content

Commit fbad9eb

Browse files
committed
Implement tasks table of contents
1 parent 7846597 commit fbad9eb

File tree

9 files changed

+244
-109
lines changed

9 files changed

+244
-109
lines changed

app/Models/Post.php

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
/**
6+
* @property string $content
7+
*/
8+
class Post extends StatamicModel
9+
{
10+
public function summary(): string
11+
{
12+
$summary = substr($this->content, 0, strpos($this->content, '<h2') ?: 0);
13+
$summary = preg_replace('/<a(\s|>)[^>]*>(.*?)<\/a>/', '$2', $summary) ?: '';
14+
$summary = preg_replace('/<img[^>]*>/', '', $summary) ?: '';
15+
16+
return $summary;
17+
}
18+
19+
public function duration(): int
20+
{
21+
$words = str_word_count(strip_tags($this->content));
22+
23+
return intval(round($words / 200));
24+
}
25+
26+
/**
27+
* @return array<Landmark>
28+
*/
29+
public function landmarks(): array
30+
{
31+
return parse_landmarks($this->content);
32+
}
33+
}

app/Models/Project.php

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Illuminate\Support\Facades\File;
6+
7+
/**
8+
* @method string value(string $key)
9+
* @method string id()
10+
*/
11+
class Project extends StatamicModel
12+
{
13+
public function stateClasses(): string
14+
{
15+
switch ($this->value('state')) {
16+
case 'live':
17+
return 'bg-jade-lighter text-jade-darker';
18+
case 'archived':
19+
case 'experimental':
20+
return 'bg-yellow-lighter text-yellow-darker';
21+
default:
22+
return 'bg-blue-lighter text-blue-darker';
23+
}
24+
}
25+
26+
/**
27+
* @return array<array{url: string, description: string}>
28+
*/
29+
public function images(): array
30+
{
31+
$id = $this->id();
32+
$project = substr($id, 0, strlen($id) - 8);
33+
$imagesPath = "img/projects/{$project}/images";
34+
35+
/** @var array<array{url: string, description: string}> */
36+
$images = collect(File::files(public_path($imagesPath)))
37+
->map(function ($file, $index) use ($imagesPath) {
38+
$filename = $file->getFilename();
39+
$number = $index + 1;
40+
41+
return [
42+
'url' => "/{$imagesPath}/{$filename}",
43+
'description' => "Project image ({$number})",
44+
];
45+
})
46+
->sortBy('url')
47+
->toArray();
48+
49+
return $images;
50+
}
51+
}

app/Models/StatamicModel.php

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Illuminate\Support\Str;
6+
use Statamic\Entries\Entry;
7+
use Statamic\Facades\Collection;
8+
9+
abstract class StatamicModel
10+
{
11+
private Entry $entry;
12+
13+
public function __construct(Entry $entry)
14+
{
15+
$this->entry = $entry;
16+
}
17+
18+
public static function boot(): void
19+
{
20+
$methods = array_diff(get_class_methods(static::class), ['__construct', '__get', '__call', 'boot']);
21+
$collection = Str::snake(Str::pluralStudly(class_basename(static::class)));
22+
23+
foreach ($methods as $method) {
24+
Collection::computed($collection, $method, function (Entry $entry) use ($method) {
25+
// @phpstan-ignore-next-line
26+
return (new static($entry))->{$method}();
27+
});
28+
}
29+
}
30+
31+
public function __get(string $key): mixed
32+
{
33+
return $this->entry->{$key};
34+
}
35+
36+
/**
37+
* @param array<mixed> $arguments
38+
*/
39+
public function __call(string $method, array $arguments): mixed
40+
{
41+
return $this->entry->{$method}(...$arguments);
42+
}
43+
}

app/Models/Task.php

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Carbon\Carbon;
6+
use Statamic\Entries\EntryCollection;
7+
use Statamic\Facades\Entry;
8+
9+
/**
10+
* @property Carbon $publication_date
11+
* @property Carbon $completion_date
12+
*/
13+
class Task extends StatamicModel
14+
{
15+
/**
16+
* @return EntryCollection<Comment>
17+
*/
18+
public function comments(): EntryCollection
19+
{
20+
$comments = Entry::query()->where('collection', 'comments')->where('task', 'entry::' . $this->id())->get();
21+
22+
$comments->push(Entry::make()->collection('comments')->id('entry::' . $this->id() . '-started')->data([
23+
'publication_date' => $this->publication_date->copy()->subSeconds(1),
24+
'content' => '<p class="flex items-center md:m-0">' .
25+
'Task started ' .
26+
antlers_icon('task-started', 'size-4 ml-2') .
27+
'</p>',
28+
]));
29+
30+
if (! is_null($this->completion_date)) {
31+
$comments->push(Entry::make()->collection('comments')->id('entry::' . $this->id() . '-completed')->data([
32+
'publication_date' => $this->completion_date,
33+
'content' => '<p class="flex items-center md:m-0">' .
34+
'Task completed ' .
35+
antlers_icon('task-completed', 'size-4 ml-2') .
36+
'</p>',
37+
]));
38+
}
39+
40+
return $comments->sortBy('publication_date')->values();
41+
}
42+
43+
/**
44+
* @return array<Landmark>
45+
*/
46+
public function landmarks(): array
47+
{
48+
$startDate = $this->publication_date->copy()->subSeconds(1);
49+
50+
return collect($this->comments())
51+
->values()
52+
->map(function ($comment, $index) use ($startDate) {
53+
$title = $comment->publication_date->display('datetime-short');
54+
55+
if ($comment->publication_date->eq($startDate)) {
56+
$icon = antlers_icon('task-started', 'size-4 mr-2');
57+
} elseif (! is_null($this->completion_date) && $comment->publication_date->eq($this->completion_date)) {
58+
$icon = antlers_icon('task-completed', 'size-4 mr-2');
59+
} elseif (! is_null($this->completion_date) && $comment->publication_date->gt($this->completion_date)) {
60+
$icon = antlers_icon('checkmark', 'size-4 mr-2 text-jade-darker fill-current');
61+
} else {
62+
$icon = antlers_icon('timer', 'size-4 mr-2 text-blue-darker fill-current');
63+
}
64+
65+
return (object) [
66+
'level' => 2,
67+
'title' => "<div class=\"flex font-mono\">{$icon} <span>{$title}</span></div>",
68+
'anchor' => '#comment-' . ($index + 1),
69+
];
70+
})
71+
->toArray();
72+
}
73+
}

app/Providers/AppServiceProvider.php

+8-70
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
namespace App\Providers;
44

5+
use App\Models\Post;
6+
use App\Models\Project;
7+
use App\Models\Task;
58
use App\Services\ActivityService;
69
use Carbon\Carbon;
7-
use Illuminate\Support\Facades\File;
810
use Illuminate\Support\ServiceProvider;
9-
use Statamic\Entries\Entry;
10-
use Statamic\Facades\Collection;
1111

1212
class AppServiceProvider extends ServiceProvider
1313
{
@@ -19,7 +19,7 @@ public function register(): void
1919
public function boot(): void
2020
{
2121
$this->bootCarbon();
22-
$this->bootCollections();
22+
$this->bootModels();
2323
}
2424

2525
protected function bootCarbon(): void
@@ -48,72 +48,10 @@ protected function bootCarbon(): void
4848
});
4949
}
5050

51-
protected function bootCollections(): void
51+
protected function bootModels(): void
5252
{
53-
Collection::computed('posts', 'summary', function (Entry $post) {
54-
if (! isset($post->content) || ! is_string($post->content)) {
55-
return null;
56-
}
57-
58-
$summary = substr($post->content, 0, strpos($post->content, '<h2') ?: 0);
59-
$summary = preg_replace('/<a(\s|>)[^>]*>(.*?)<\/a>/', '$2', $summary) ?: '';
60-
$summary = preg_replace('/<img[^>]*>/', '', $summary);
61-
62-
return $summary;
63-
});
64-
65-
Collection::computed('posts', 'duration', function (Entry $post) {
66-
if (! isset($post->content) || ! is_string($post->content)) {
67-
return null;
68-
}
69-
70-
$words = str_word_count(strip_tags($post->content));
71-
72-
return round($words / 200);
73-
});
74-
75-
Collection::computed('posts', 'landmarks', function (Entry $post) {
76-
if (! isset($post->content) || ! is_string($post->content)) {
77-
return null;
78-
}
79-
80-
return parse_landmarks($post->content);
81-
});
82-
83-
Collection::computed('projects', 'stateClasses', function (Entry $project) {
84-
switch ($project->value('state')) {
85-
case 'live':
86-
return 'bg-jade-lighter text-jade-darker';
87-
case 'archived':
88-
case 'experimental':
89-
return 'bg-yellow-lighter text-yellow-darker';
90-
default:
91-
return 'bg-blue-lighter text-blue-darker';
92-
}
93-
});
94-
95-
Collection::computed('projects', 'images', function (Entry $project) {
96-
$id = $project->id();
97-
98-
if (! is_string($id)) {
99-
return [];
100-
}
101-
102-
$project = substr($id, 0, strlen($id) - 8);
103-
$imagesPath = "img/projects/{$project}/images";
104-
105-
return collect(File::files(public_path($imagesPath)))
106-
->map(function ($file, $index) use ($imagesPath) {
107-
$filename = $file->getFilename();
108-
$number = $index + 1;
109-
110-
return [
111-
'url' => "/{$imagesPath}/{$filename}",
112-
'description' => "Project image ({$number})",
113-
];
114-
})
115-
->sortBy('url')
116-
->toArray();
117-
});
53+
Post::boot();
54+
Task::boot();
55+
Project::boot();
11856
}
11957
}

app/Support/helpers.php

+16
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ function _clean_landmark($landmark)
149149
}
150150
}
151151

152+
/**
153+
* @return array<Landmark>
154+
*/
152155
function parse_landmarks(string $html): array
153156
{
154157
$headings = _parse_html_headings($html);
@@ -167,3 +170,16 @@ function parse_landmarks(string $html): array
167170
}
168171

169172
}
173+
174+
if (! function_exists('antlers_icon')) {
175+
function antlers_icon(string $name, string $class = '', array $attrs = []): string
176+
{
177+
$attrsString = "class=\"{$class}\"";
178+
179+
foreach ($attrs as $attr => $value) {
180+
$attrsString .= " {$attr}=\"{$value}\"";
181+
}
182+
183+
return str_replace('class="{{ class ?? \'\' }}"', $attrsString, file_get_contents(resource_path("views/icons/{$name}.antlers.html")));
184+
}
185+
}

phpstan.neon

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ parameters:
44
level: max
55
paths:
66
- app/
7+
universalObjectCratesClasses:
8+
- Statamic\Entries\Entry
9+
typeAliases:
10+
Landmark: 'object{level: int, title: string, anchor: string, parent: object, children: array<object>}'
711
excludePaths:
12+
- app/Models/Task.php
813
- app/Services/ActivityService.php
914
- app/Support/helpers.php

resources/views/components/table-of-contents.blade.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class="bg-overlay-dark fixed inset-0 z-10"
6464

6565
<button
6666
type="button"
67-
class="group right-[calc(max(0px,(100vw-theme('maxWidth.content'))/2))] fixed top-0 mt-4 mr-4 flex h-12 w-12 items-center justify-center md:mt-16 md:-mr-1 md:h-8 md:w-8"
67+
class="group right-[calc(max(0px,(100vw-theme('maxWidth.content'))/2))] fixed top-0 mt-4 mr-4 flex h-12 w-12 items-center justify-center md:mt-16 md:h-8 md:w-8"
6868
@click="open()"
6969
>
7070
<div

0 commit comments

Comments
 (0)