diff --git a/app/Icons/Android.php b/app/Icons/Android.php new file mode 100644 index 0000000..5a5619a --- /dev/null +++ b/app/Icons/Android.php @@ -0,0 +1,2145 @@ + '9.2k+', + 'online' => '184', + 'mensagens' => '1.3k', + ]; - public ?string $username = null; + /** + * Mock — Eventos vem de docs/plans/2026-09-22-api-mobile-jwt.md + * (feature "Eventos", ainda com gap de domínio pro check-in por QR). + * `statusToken` é um token de cor do tema (`primary` | `success`). + * + * @var list + */ + public array $upcomingEvents = [ + [ + 'title' => 'Workshop: Laravel na prática', + 'when' => 'Qui, 25 Set · 19h', + 'status' => 'Em breve', + 'statusToken' => 'primary', + ], + [ + 'title' => 'Mentoria coletiva de carreira', + 'when' => 'Sáb, 27 Set · 10h', + 'status' => 'Vagas abertas', + 'statusToken' => 'success', + ], + ]; - public function mount(): void - { - $response = Http::withToken((string) config('services.he4rt_api.poc_token')) - ->get(config('services.he4rt_api.base_url').'/api/mobile/me'); + /** + * Mock — post fixado da Timeline, com um trecho de código pra dar cara + * 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, tags: list, likes: int, comments: int} + */ + public array $featuredPost = [ + 'author' => 'time He4rt', + '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', + ], + 'tags' => ['#backend', '#postgres', '#performance'], + 'likes' => 84, + 'comments' => 23, + ]; - if ($response->successful()) { - $this->status = 'ok'; - $this->username = $response->json('username'); + /** + * Mock — restante da Timeline, em formato compacto. + * + * @var list + */ + public array $latestPosts = [ + [ + 'title' => 'Turma de mentoria de Setembro está com vagas abertas', + 'author' => 'maria.dev', + 'timeAgo' => 'há 5h', + ], + [ + 'title' => 'Como fomos de 0 a 9 mil devs na comunidade', + 'author' => 'time He4rt', + 'timeAgo' => 'ontem', + ], + ]; - return; - } + public function goToEventos(): void + { + $this->replace('/eventos'); + } - $this->status = 'error'; + public function goToTimeline(): void + { + $this->replace('/timeline'); + } + + public function goToDiscord(): void + { + Browser::open('https://discord.com/invite/he4rt'); } public function navTitle(): string { - return 'He4rt'; + return 'Início'; } public function render(): View diff --git a/app/NativeComponents/Perfil.php b/app/NativeComponents/Perfil.php new file mode 100644 index 0000000..9666da2 --- /dev/null +++ b/app/NativeComponents/Perfil.php @@ -0,0 +1,41 @@ +get(config('services.he4rt_api.base_url').'/api/mobile/me'); + + if ($response->successful()) { + $this->apiStatus = 'ok'; + $this->apiUsername = $response->json('username'); + + return; + } + + $this->apiStatus = 'error'; + } + + public function navTitle(): string + { + return 'Perfil'; + } + + public function render(): View + { + return view('native.perfil'); + } +} diff --git a/app/NativeComponents/Timeline.php b/app/NativeComponents/Timeline.php new file mode 100644 index 0000000..082b26f --- /dev/null +++ b/app/NativeComponents/Timeline.php @@ -0,0 +1,21 @@ +titleView(view('native.partials.brand-title')) + ->displayMode('inline'); + } + + public function tabBar(NativeComponent $screen): ?TabBar + { + return TabBar::make() + ->activeColor(theme('primary')) + ->add(Tab::link('Início', '/home', ios: Ios::HouseFill, android: Android::Home)) + ->add(Tab::link('Timeline', '/timeline', ios: Ios::BubbleLeftAndBubbleRightFill, android: Android::Forum)) + ->add(Tab::link('Eventos', '/eventos', ios: Ios::Calendar, android: Android::Event)) + ->add(Tab::link('Perfil', '/perfil', ios: Ios::PersonFill, android: Android::AccountCircle)); + } + + public function usesNativeChrome(): bool + { + return true; + } +} diff --git a/config/nativephp.php b/config/nativephp.php index 76698f4..025afbe 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -128,7 +128,7 @@ | */ - 'appearance' => env('NATIVEPHP_APPEARANCE', 'system'), + 'appearance' => env('NATIVEPHP_APPEARANCE', 'dark'), /* |-------------------------------------------------------------------------- diff --git a/resources/views/native/eventos.blade.php b/resources/views/native/eventos.blade.php new file mode 100644 index 0000000..4dcb73e --- /dev/null +++ b/resources/views/native/eventos.blade.php @@ -0,0 +1,10 @@ +@use('App\Icons\Ios') +@use('App\Icons\Android') + +@include('native.partials.coming-soon', [ + 'icon' => 'calendar', + 'iosIcon' => Ios::Calendar, + 'androidIcon' => Android::Event, + 'title' => 'Eventos chegando em breve', + 'description' => "Workshops, mentorias e encontros da comunidade — com inscrição e check-in direto pelo app.", +]) diff --git a/resources/views/native/home.blade.php b/resources/views/native/home.blade.php index c70de5e..91d3ecd 100644 --- a/resources/views/native/home.blade.php +++ b/resources/views/native/home.blade.php @@ -1,11 +1,159 @@ - - @if ($status === 'loading') - - Chamando a API do He4rt... - @elseif ($status === 'ok') - Pong, {{ $username }}! - /api/mobile/me respondeu via Sanctum - @else - Falha ao chamar a API - @endif - +@use('App\Icons\Ios') +@use('App\Icons\Android') + + + + + {{-- Hero: card de boas-vindas com gradiente da marca, não a réplica do terminal do site --}} + + + + Bem-vindo de volta + Comunidade He4rt + + + + + + + + + Membros + {{ $stats['membros'] }} + + + Online agora + {{ $stats['online'] }} + + + Mensagens + {{ $stats['mensagens'] }} + + + + + + + + Acessar Discord da He4rt + + + + + + + {{-- Próximos eventos --}} + + + + + Próximos eventos + + + Ver todos + + + + + @foreach ($upcomingEvents as $event) + + + + + + {{ $event['status'] }} + + + {{ $event['title'] }} + + + + + {{ $event['when'] }} + + + + @endforeach + + + + {{-- Timeline / comunidade --}} + + + + + Da comunidade + + + Ver timeline + + + + {{-- Post em destaque --}} + + + + + + + + {{ $featuredPost['author'] }} + + CORE + + + {{ $featuredPost['timeAgo'] }} + + + + {{ $featuredPost['body'] }} + + + + {{ $featuredPost['codeFile'] }} + {{ $featuredPost['codeLang'] }} + + @foreach ($featuredPost['codeLines'] as $line) + {{ $line }} + @endforeach + + + + @foreach ($featuredPost['tags'] as $tag) + + {{ $tag }} + + @endforeach + + + + + + {{ $featuredPost['likes'] }} + + + + {{ $featuredPost['comments'] }} + + + + + + + + @foreach ($latestPosts as $post) + + @endforeach + + + + + + diff --git a/resources/views/native/partials/brand-title.blade.php b/resources/views/native/partials/brand-title.blade.php new file mode 100644 index 0000000..a4afdbf --- /dev/null +++ b/resources/views/native/partials/brand-title.blade.php @@ -0,0 +1,9 @@ +@use('App\Icons\Ios') +@use('App\Icons\Android') + + + + + + He4rt Devs + diff --git a/resources/views/native/partials/coming-soon.blade.php b/resources/views/native/partials/coming-soon.blade.php new file mode 100644 index 0000000..d1755a3 --- /dev/null +++ b/resources/views/native/partials/coming-soon.blade.php @@ -0,0 +1,15 @@ +{{-- + Estado "em construção" compartilhado pelas abas cuja API real (ver + docs/plans/2026-09-22-api-mobile-jwt.md no heartdevs.com) ainda não + existe. Espera $icon/$iosIcon/$androidIcon, $title, $description. +--}} + + + + + + {{ $title }} + {{ $description }} + + + diff --git a/resources/views/native/perfil.blade.php b/resources/views/native/perfil.blade.php new file mode 100644 index 0000000..4836866 --- /dev/null +++ b/resources/views/native/perfil.blade.php @@ -0,0 +1,44 @@ +@use('App\Icons\Ios') +@use('App\Icons\Android') + + + + + + + + + + + Convidado + Login com a comunidade chegando em breve + + + + + Conexão com a API + + + @if ($apiStatus === 'loading') + + + Conectando... + GET /api/mobile/me + + @elseif ($apiStatus === 'ok') + + + Conectado como {{ $apiUsername }} + GET /api/mobile/me · Sanctum (POC) + + @else + + + Falha ao conectar com a API + GET /api/mobile/me · Sanctum (POC) + + @endif + + + + diff --git a/resources/views/native/timeline.blade.php b/resources/views/native/timeline.blade.php new file mode 100644 index 0000000..54efa17 --- /dev/null +++ b/resources/views/native/timeline.blade.php @@ -0,0 +1,10 @@ +@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.", +]) diff --git a/routes/web.php b/routes/web.php index 652eee0..f5438df 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,12 +2,22 @@ declare(strict_types=1); +use App\NativeComponents\Eventos; use App\NativeComponents\Home; +use App\NativeComponents\Perfil; use App\NativeComponents\Splash; +use App\NativeComponents\Timeline; +use App\NativeLayouts\TabsLayout; use Illuminate\Support\Facades\Route; Route::native('/', Splash::class); -Route::native('/home', Home::class); + +Route::nativeGroup(layout: TabsLayout::class, routes: function (): void { + Route::native('/home', Home::class); + Route::native('/timeline', Timeline::class); + Route::native('/eventos', Eventos::class); + Route::native('/perfil', Perfil::class); +}); // Servida pro da Splash, não é uma tela nativa. Route::view('/web/splash-animation', 'web.splash-animation'); diff --git a/tests/Feature/NativeHomeTest.php b/tests/Feature/NativeHomeTest.php index b252cb3..5844e6f 100644 --- a/tests/Feature/NativeHomeTest.php +++ b/tests/Feature/NativeHomeTest.php @@ -3,25 +3,34 @@ declare(strict_types=1); use App\NativeComponents\Home; -use Illuminate\Support\Facades\Http; use Native\Mobile\Testing\Native; it('sets the nav title', function () { - expect((new Home)->navTitle())->toBe('He4rt'); + expect((new Home)->navTitle())->toBe('Início'); }); -it('shows the username after a successful API call', function () { - Http::fake([ - '*/api/mobile/me' => Http::response(['id' => '1', 'username' => 'danielhe4rt']), - ]); +it('shows the community stats and mocked sections', function () { + Native::visit('/home') + ->assertSee('Comunidade He4rt') + ->assertSee('Próximos eventos') + ->assertSee('Workshop: Laravel na prática') + ->assertSee('Da comunidade'); +}); - Native::visit('/home')->assertSee('Pong, danielhe4rt!'); +it('replaces itself with eventos when going to the events teaser', function () { + Native::visit('/home') + ->call('goToEventos') + ->assertReplacedWith('/eventos'); }); -it('shows an error state when the API call fails', function () { - Http::fake([ - '*/api/mobile/me' => Http::response(null, 401), - ]); +it('replaces itself with timeline when going to the community teaser', function () { + Native::visit('/home') + ->call('goToTimeline') + ->assertReplacedWith('/timeline'); +}); - Native::visit('/home')->assertSee('Falha ao chamar a API'); +it('opens the He4rt Discord invite', function () { + Native::visit('/home') + ->call('goToDiscord') + ->assertNativeCalled('Browser.Open', fn (array $params) => $params['url'] === 'https://discord.com/invite/he4rt'); }); diff --git a/tests/Feature/PerfilTest.php b/tests/Feature/PerfilTest.php new file mode 100644 index 0000000..876abbc --- /dev/null +++ b/tests/Feature/PerfilTest.php @@ -0,0 +1,27 @@ +navTitle())->toBe('Perfil'); +}); + +it('shows the username after a successful API call', function () { + Http::fake([ + '*/api/mobile/me' => Http::response(['id' => '1', 'username' => 'danielhe4rt']), + ]); + + Native::visit('/perfil')->assertSee('Conectado como danielhe4rt'); +}); + +it('shows an error state when the API call fails', function () { + Http::fake([ + '*/api/mobile/me' => Http::response(null, 401), + ]); + + Native::visit('/perfil')->assertSee('Falha ao conectar com a API'); +});