Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: article view route #204

Merged
merged 5 commits into from
Oct 10, 2023
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
2 changes: 1 addition & 1 deletion app/Data/Articles/FeaturedCollectionData.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static function fromModel(Collection $collection): self
{
return new self(
name: $collection->name,
image: $collection->image
image: $collection->image(),
);
}
}
15 changes: 15 additions & 0 deletions app/Http/Controllers/ArticleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,19 @@ public function index(Request $request): Response|JsonResponse
'title' => trans('metatags.articles.title'),
]);
}

public function show(Article $article): Response
{
if ($article->isNotPublished()) {
abort(404);
}

return Inertia::render('Articles/Show', [
'article' => ArticleData::fromModel($article),
])->withViewData([
'title' => trans('metatags.articles.view.title', ['title' => $article->title]),
'description' => $article->metaDescription(),
'image' => $article->getMedia()->first()->getUrl(),
]);
}
}
11 changes: 11 additions & 0 deletions app/Models/Article.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\Sluggable\HasSlug;
Expand Down Expand Up @@ -68,6 +69,16 @@ public function scopeSortById(Builder $query): Builder
return $query->orderBy('articles.id', 'desc');
}

public function metaDescription(): string
{
return $this->meta_description ?? Str::limit(strip_tags($this->content), 157);
}

public function isNotPublished(): bool
{
return $this->published_at === null || $this->published_at->isFuture();
}

public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
Expand Down
3 changes: 3 additions & 0 deletions lang/en/metatags.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@

'articles' => [
'title' => 'Articles | Dashbrd',
'view' => [
'title' => ':title | Dashbrd',
],
],

'login' => [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const ArticleCard = ({
return (
<a
data-testid="ArticleCard"
href={`/articles/${article.slug}`}
href={route("articles.view", article.slug)}
className={cn("group flex w-full flex-col overflow-hidden rounded-xl border border-theme-secondary-300", {
"bg-white": !isLargeVariant,
"bg-theme-dark-900": isLargeVariant,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const ArticleListItem = ({ article }: { article: App.Data.Articles.Articl
return (
<a
data-testid="ArticleListItem"
href={`/articles/${article.slug}`}
href={route("articles.view", article.slug)}
className="flex space-x-3 border-b-4 border-theme-secondary-100 bg-white p-6 lg:rounded-lg lg:border lg:border-theme-secondary-300"
>
<div className="aspect-video h-11 flex-shrink-0 overflow-hidden rounded bg-theme-secondary-300 sm:h-16">
Expand Down
2 changes: 1 addition & 1 deletion resources/js/I18n/Locales/en.json

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions resources/js/Pages/Articles/Show.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { DefaultLayout } from "@/Layouts/DefaultLayout";

const ArticlesShow = ({ article }: { article: App.Data.Articles.ArticleData }): JSX.Element => (
<DefaultLayout>
<div className="mx-6 sm:mx-8 2xl:mx-0">{article.title}</div>
</DefaultLayout>
);

export default ArticlesShow;
1 change: 1 addition & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@

Route::group(['prefix' => 'articles', 'middleware' => 'features:articles'], function () {
Route::get('', [ArticleController::class, 'index'])->name('articles');
Route::get('{article:slug}', [ArticleController::class, 'show'])->name('articles.view');
});

Route::group(['prefix' => 'collections', 'middleware' => 'features:collections'], function () {
Expand Down
16 changes: 16 additions & 0 deletions tests/App/Http/Controllers/ArticleControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ function seedArticles(int $articlesCount = 4, int $collectionsCount = 2): array
->assertStatus(200);
});

it('should render a single published article', function () {
[, [$article]] = seedArticles(1);

$this->get(route('articles.view', ['article' => $article->slug]))
->assertStatus(200);
});

it('should not render a single unpublished article', function () {
[, [$article]] = seedArticles(1);

$article->update(['published_at' => null]);

$this->get(route('articles.view', ['article' => $article->slug]))
->assertStatus(404);
});

it('should return articles with the given pageLimit', function ($pageLimit, $resultCount) {
seedArticles(35, 4);

Expand Down
59 changes: 59 additions & 0 deletions tests/App/Models/ArticleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,62 @@
expect($articles[0]->id)->toBe($createdArticles[1]->id)
->and($articles[1]->id)->toBe($createdArticles[0]->id);
});

it('gets meta description from attribute', function () {
$article = Article::factory()->create([
'meta_description' => 'This is a meta description',
'content' => 'This is the content',
]);

expect($article->metaDescription())->toBe('This is a meta description');
});

it('gets meta description from content if meta description is not set', function () {
$article = Article::factory()->create([
'meta_description' => null,
'content' => 'This is the content',
]);

expect($article->metaDescription())->toBe('This is the content');
});

it('truncates the content if used as meta description', function () {
$article = Article::factory()->create([
'meta_description' => null,
'content' => str_repeat('a', 300),
]);

expect($article->metaDescription())->toHaveLength(160);
});

it('determines that article is published if published_at is in the past', function () {
$article = Article::factory()->create([
'published_at' => now()->subDay(),
]);

expect($article->isNotPublished())->toBeFalse();
});

it('determines that article is published if published_at is now', function () {
$article = Article::factory()->create([
'published_at' => now(),
]);

expect($article->isNotPublished())->toBeFalse();
});

it('determines that article is not published if published_at is in the future', function () {
$article = Article::factory()->create([
'published_at' => now()->addDay(),
]);

expect($article->isNotPublished())->toBeTrue();
});

it('determines that article is not published if published_at is null', function () {
$article = Article::factory()->create([
'published_at' => null,
]);

expect($article->isNotPublished())->toBeTrue();
});
Loading