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
15 changes: 9 additions & 6 deletions app/NativeComponents/Home.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,20 @@ class Home extends NativeComponent
* de comunidade dev. Vem de docs/plans/2026-09-22-api-mobile-jwt.md
* (feature "Timeline").
*
* @var array{author: string, timeAgo: string, body: string, codeFile: string, codeLang: string, codeLines: list<string>, tags: list<string>, likes: int, comments: int}
* @var array{author: string, role: ?string, timeAgo: string, body: string, code: ?array{file: string, lang: string, lines: list<string>}, tags: list<string>, likes: int, comments: int}
*/
public array $featuredPost = [
'author' => 'time He4rt',
'role' => 'CORE',
'timeAgo' => 'há 2h',
'body' => 'Refatoramos o pool de conexões do Postgres no gateway da API do heartdevs.com.',
'codeFile' => 'config/database.php',
'codeLang' => 'php',
'codeLines' => [
"'pool' => ['min' => 5, 'max' => 50],",
'// timeout ajustado pra reduzir o P99',
'code' => [
'file' => 'config/database.php',
'lang' => 'php',
'lines' => [
"'pool' => ['min' => 5, 'max' => 50],",
'// timeout ajustado pra reduzir o P99',
],
],
'tags' => ['#backend', '#postgres', '#performance'],
'likes' => 84,
Expand Down
97 changes: 96 additions & 1 deletion app/NativeComponents/Timeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,108 @@

class Timeline extends NativeComponent
{
public string $activeFilter = 'todos';

/**
* Mock — a Timeline real vem de docs/plans/2026-09-22-api-mobile-jwt.md
* no heartdevs.com (feature "Timeline"), ainda não existe API pra isso.
*
* @var list<array{author: string, role: ?string, timeAgo: string, body: string, code: ?array{file: string, lang: string, lines: list<string>}, tags: list<string>, likes: int, comments: int, category: string}>
*/
public array $posts = [
[
'author' => 'time He4rt',
'role' => 'CORE',
'timeAgo' => 'há 2h',
'body' => 'Refatoramos o pool de conexões do Postgres no gateway da API do heartdevs.com.',
'code' => [
'file' => 'config/database.php',
'lang' => 'php',
'lines' => [
"'pool' => ['min' => 5, 'max' => 50],",
'// timeout ajustado pra reduzir o P99',
],
],
'tags' => ['#backend', '#postgres', '#performance'],
'likes' => 84,
'comments' => 23,
'category' => 'projetos',
],
[
'author' => 'maria.dev',
'role' => null,
'timeAgo' => 'há 5h',
'body' => 'A turma de mentoria de Setembro está com vagas abertas! Inscrições abertas até o dia 30.',
'code' => null,
'tags' => ['#mentoria', '#carreira'],
'likes' => 41,
'comments' => 12,
'category' => 'mentorias',
],
[
'author' => 'time He4rt',
'role' => 'CORE',
'timeAgo' => 'ontem',
'body' => 'Como fomos de 0 a 9 mil devs na comunidade — um resumo da jornada até aqui.',
'code' => null,
'tags' => [],
'likes' => 156,
'comments' => 37,
'category' => 'geral',
],
[
'author' => 'pedro.silva',
'role' => null,
'timeAgo' => 'há 2 dias',
'body' => 'Lancei meu primeiro projeto open source usando Laravel + Livewire. Feedback é bem-vindo.',
'code' => null,
'tags' => ['#laravel', '#livewire', '#opensource'],
'likes' => 62,
'comments' => 19,
'category' => 'projetos',
],
[
'author' => 'time He4rt',
'role' => 'CORE',
'timeAgo' => 'há 3 dias',
'body' => 'A He4rt Conf 2025 está confirmada pra Outubro, com inscrições abertas em breve.',
'code' => null,
'tags' => ['#he4rtconf'],
'likes' => 203,
'comments' => 58,
'category' => 'eventos',
],
];

public function setFilter(string $filter): void
{
$this->activeFilter = $filter;
}

/**
* @return list<array{author: string, role: ?string, timeAgo: string, body: string, code: ?array{file: string, lang: string, lines: list<string>}, tags: list<string>, likes: int, comments: int, category: string}>
*/
public function filteredPosts(): array
{
if ($this->activeFilter === 'todos') {
return $this->posts;
}

return array_values(array_filter(
$this->posts,
fn (array $post): bool => $post['category'] === $this->activeFilter,
));
}

public function navTitle(): string
{
return 'Timeline';
}

public function render(): View
{
return view('native.timeline');
return view('native.timeline', [
'posts' => $this->filteredPosts(),
]);
}
}
52 changes: 2 additions & 50 deletions resources/views/native/home.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,56 +89,8 @@
</pressable>
</row>

{{-- Post em destaque --}}
<column class="w-full gap-3 bg-theme-surface border border-theme-outline-variant/60 rounded-[20] p-4">
<row class="items-center gap-3">
<column class="w-10 h-10 items-center justify-center bg-theme-primary/15 rounded-full">
<icon name="person.fill" :ios="Ios::PersonFill" :android="Android::AccountCircle" size="18" class="text-theme-primary" />
</column>
<column class="flex-1 gap-0.5">
<row class="items-center gap-2">
<text class="text-theme-on-surface font-bold">{{ $featuredPost['author'] }}</text>
<row class="items-center bg-theme-accent/15 rounded-full px-2 py-0.5">
<text class="text-xs text-theme-accent font-bold">CORE</text>
</row>
</row>
<text class="text-xs text-theme-on-surface-variant">{{ $featuredPost['timeAgo'] }}</text>
</column>
</row>

<text class="text-sm text-theme-on-surface">{{ $featuredPost['body'] }}</text>

<column class="w-full gap-1 bg-theme-background border border-theme-outline-variant/40 rounded-[12] p-3">
<row class="w-full items-center justify-between">
<text class="text-xs text-theme-on-surface-variant" font="mono">{{ $featuredPost['codeFile'] }}</text>
<text class="text-xs text-theme-accent uppercase tracking-wide" font="mono">{{ $featuredPost['codeLang'] }}</text>
</row>
@foreach ($featuredPost['codeLines'] as $line)
<text class="text-xs text-theme-on-surface-variant" font="mono">{{ $line }}</text>
@endforeach
</column>

<row class="gap-2">
@foreach ($featuredPost['tags'] as $tag)
<row class="bg-theme-surface-variant rounded-full px-2.5 py-1">
<text class="text-xs text-theme-on-surface-variant">{{ $tag }}</text>
</row>
@endforeach
</row>

<row class="items-center gap-4 mt-1">
<row class="items-center gap-1.5">
<icon name="heart" :ios="Ios::Heart" :android="Android::FavoriteBorder" size="15" class="text-theme-on-surface-variant" />
<text class="text-xs text-theme-on-surface-variant">{{ $featuredPost['likes'] }}</text>
</row>
<row class="items-center gap-1.5">
<icon name="bubble.left" :ios="Ios::BubbleLeft" :android="Android::ChatBubbleOutline" size="15" class="text-theme-on-surface-variant" />
<text class="text-xs text-theme-on-surface-variant">{{ $featuredPost['comments'] }}</text>
</row>
<icon name="bookmark" :ios="Ios::Bookmark" :android="Android::BookmarkBorder" size="15" class="text-theme-on-surface-variant" />
<icon name="square.and.arrow.up" :ios="Ios::SquareAndArrowUp" :android="Android::Share" size="15" class="text-theme-on-surface-variant" />
</row>
</column>
{{-- Post em destaque — mesmo card reutilizável da Timeline --}}
@include('native.partials.post-card', ['post' => $featuredPost])

<column class="w-full gap-2">
@foreach ($latestPosts as $post)
Expand Down
63 changes: 63 additions & 0 deletions resources/views/native/partials/post-card.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@use('App\Icons\Ios')
@use('App\Icons\Android')

{{--
Card de post reutilizável da Timeline. Espera $post com as chaves:
author, role (nullable), timeAgo, body, code (nullable: file/lang/lines),
tags (array), likes, comments.
--}}
<column class="w-full gap-3 bg-theme-surface border border-theme-outline-variant/60 rounded-[20] p-4">
<row class="items-center gap-3">
<column class="w-10 h-10 items-center justify-center bg-theme-primary/15 rounded-full">
<icon name="person.fill" :ios="Ios::PersonFill" :android="Android::AccountCircle" size="18" class="text-theme-primary" />
</column>
<column class="flex-1 gap-0.5">
<row class="items-center gap-2">
<text class="text-theme-on-surface font-bold">{{ $post['author'] }}</text>
@if (! empty($post['role']))
<row class="items-center bg-theme-accent/15 rounded-full px-2 py-0.5">
<text class="text-xs text-theme-accent font-bold">{{ $post['role'] }}</text>
</row>
@endif
</row>
<text class="text-xs text-theme-on-surface-variant">{{ $post['timeAgo'] }}</text>
</column>
</row>

<text class="text-sm text-theme-on-surface">{{ $post['body'] }}</text>

@if (! empty($post['code']))
<column class="w-full gap-1 bg-theme-background border border-theme-outline-variant/40 rounded-[12] p-3">
<row class="w-full items-center justify-between">
<text class="text-xs text-theme-on-surface-variant" font="mono">{{ $post['code']['file'] }}</text>
<text class="text-xs text-theme-accent uppercase tracking-wide" font="mono">{{ $post['code']['lang'] }}</text>
</row>
@foreach ($post['code']['lines'] as $line)
<text class="text-xs text-theme-on-surface-variant" font="mono">{{ $line }}</text>
@endforeach
</column>
@endif

@if (! empty($post['tags']))
<row class="gap-2">
@foreach ($post['tags'] as $tag)
<row class="bg-theme-surface-variant rounded-full px-2.5 py-1">
<text class="text-xs text-theme-on-surface-variant">{{ $tag }}</text>
</row>
@endforeach
</row>
@endif

<row class="items-center gap-4 mt-1">
<row class="items-center gap-1.5">
<icon name="heart" :ios="Ios::Heart" :android="Android::FavoriteBorder" size="15" class="text-theme-on-surface-variant" />
<text class="text-xs text-theme-on-surface-variant">{{ $post['likes'] }}</text>
</row>
<row class="items-center gap-1.5">
<icon name="bubble.left" :ios="Ios::BubbleLeft" :android="Android::ChatBubbleOutline" size="15" class="text-theme-on-surface-variant" />
<text class="text-xs text-theme-on-surface-variant">{{ $post['comments'] }}</text>
</row>
<icon name="bookmark" :ios="Ios::Bookmark" :android="Android::BookmarkBorder" size="15" class="text-theme-on-surface-variant" />
<icon name="square.and.arrow.up" :ios="Ios::SquareAndArrowUp" :android="Android::Share" size="15" class="text-theme-on-surface-variant" />
</row>
</column>
31 changes: 24 additions & 7 deletions resources/views/native/timeline.blade.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
@use('App\Icons\Ios')
@use('App\Icons\Android')

@include('native.partials.coming-soon', [
'icon' => 'bubble.left.and.bubble.right.fill',
'iosIcon' => Ios::BubbleLeftAndBubbleRightFill,
'androidIcon' => Android::Forum,
'title' => 'Timeline chegando em breve',
'description' => "A gente tá construindo a timeline da comunidade — publicações, projetos e novidades dos devs He4rt, direto no app.",
])
<scroll-view class="w-full h-full bg-theme-background">
<column class="w-full gap-4 p-4 pb-10">
<scroll-view horizontal class="w-full">
<row class="gap-2">
<chip label="Tudo" :selected="$activeFilter === 'todos'" @change="setFilter('todos')" />
<chip label="Projetos" :selected="$activeFilter === 'projetos'" @change="setFilter('projetos')" />
<chip label="Eventos" :selected="$activeFilter === 'eventos'" @change="setFilter('eventos')" />
<chip label="Mentorias" :selected="$activeFilter === 'mentorias'" @change="setFilter('mentorias')" />
<chip label="Geral" :selected="$activeFilter === 'geral'" @change="setFilter('geral')" />
</row>
</scroll-view>

<column class="w-full gap-3">
@forelse ($posts as $post)
@include('native.partials.post-card', ['post' => $post])
@empty
<column class="w-full items-center gap-2 p-8">
<icon name="bubble.left.and.bubble.right" :ios="Ios::BubbleLeftAndBubbleRight" :android="Android::ChatBubbleOutline" size="28" class="text-theme-on-surface-variant" />
<text class="text-sm text-theme-on-surface-variant text-center">Nada por aqui ainda nessa categoria.</text>
</column>
@endforelse
</column>
</column>
</scroll-view>
30 changes: 30 additions & 0 deletions tests/Feature/TimelineTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

use App\NativeComponents\Timeline;
use Native\Mobile\Testing\Native;

it('sets the nav title', function () {
expect((new Timeline)->navTitle())->toBe('Timeline');
});

it('shows every mocked post by default', function () {
Native::visit('/timeline')
->assertSee('Refatoramos o pool de conexões do Postgres')
->assertSee('turma de mentoria de Setembro')
->assertSee('He4rt Conf 2025');
});

it('narrows the feed when a category filter is selected', function () {
Native::visit('/timeline')
->call('setFilter', 'mentorias')
->assertSee('turma de mentoria de Setembro')
->assertDontSee('He4rt Conf 2025');
});

it('shows an empty state when a filter has no posts', function () {
Native::visit('/timeline')
->call('setFilter', 'inexistente')
->assertSee('Nada por aqui ainda nessa categoria');
});
Loading