Skip to content
Open
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
119 changes: 119 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# SourceVortex

Front-end do [sourcevortex.com.br](https://sourcevortex.com.br) em Laravel + Inertia + Vue.

Este projeto **não tem banco próprio**. Ele lê, em modo somente leitura, as tabelas de uma
instalação existente do WordPress (`wp_posts`, `wp_postmeta`, `wp_terms`, `wp_term_taxonomy`,
`wp_term_relationships`, `wp_users`, `wp_usermeta`) através de models Eloquent. O WordPress
continua sendo onde o conteúdo é escrito e administrado; aqui só se lê e se renderiza.

## Stack

| | |
|---|---|
| Back-end | Laravel 12, PHP 8.2+ |
| Front-end | Vue 3, TypeScript, Inertia 2 |
| Build | Vite 7, Tailwind 4 |
| Componentes | reka-ui (shadcn-vue) |
| Testes | Pest 3 |

## Requisitos

- PHP 8.2 ou superior
- Composer
- Node 22 ou superior
- MySQL **com as tabelas do WordPress já populadas**

## Instalação

```bash
composer install
npm ci
cp .env.example .env
php artisan key:generate
```

Aponte o `.env` para o banco do WordPress:

```dotenv
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=nome_do_banco_wordpress
DB_USERNAME=usuario
DB_PASSWORD=senha

# URL da instalação do WordPress, usada para montar os links do painel
WP_ADMIN_URL=https://exemplo.com.br
```

> **Não rode `php artisan migrate` contra o banco do WordPress** sem entender o efeito.
> As migrations do esqueleto do Laravel criam tabelas (`users`, `cache`, `jobs`) dentro
> do mesmo schema. Se `CACHE_STORE` ou `SESSION_DRIVER` estiverem em `database`, o Laravel
> passa a escrever no banco do WordPress. Prefira `redis` ou `file`.

Para desenvolver:

```bash
composer dev
```

Sobe servidor, fila, logs e Vite em paralelo.

## Testes

```bash
php artisan test
```

Os testes **não precisam** do banco do WordPress. `tests/Support/WordPressSchema.php`
cria as tabelas `wp_*` em sqlite na memória, e `tests/Support/Wp.php` traz construtores
para as linhas. Como o app não tem migration para essas tabelas, esse é o único lugar
onde o schema esperado está descrito — vale mantê-lo em dia com a instalação real.

## Qualidade

```bash
vendor/bin/pint --test # estilo PHP
npm run format:check # formatação do front
npm run lint:check # eslint
npm run type-check # vue-tsc
```

Os quatro rodam no CI e reprovam o build.

## Convenções que vêm do WordPress

Algumas regras de negócio moram no conteúdo, não no código:

| Convenção | Efeito |
|---|---|
| Categoria `destaques` | Posts nela aparecem no carrossel da home e **saem** da listagem principal |
| Menu chamado `Menu` | É o menu principal do site |
| Página com slug `politica-de-privacidade` | Alimenta a rota `/politica-de-privacidade` |

As URLs seguem o permalink padrão do WordPress (`/%year%/%monthnum%/%postname%`), o que
mantém os endereços do site antigo válidos.

## ⚠️ Dependência de plugin: Advanced Custom Fields

O subtítulo exibido na capa do post vem de `wp_postmeta` com `meta_key = 'subtitle'`.
Esse campo é criado pelo **Advanced Custom Fields** no WordPress — o tema V1 o lia com
`get_field("subtitle")`.

Isso significa que:

- o ACF precisa continuar instalado e o campo `subtitle` precisa continuar existindo;
- se o campo for movido para dentro de um *group* ou *repeater*, o `meta_key` muda de
formato e o subtítulo **some silenciosamente** aqui, sem erro nenhum.

Não há como este projeto detectar essa quebra sozinho. Se o subtítulo sumir do site,
comece a investigação pela configuração do ACF.

## Diferenças em relação ao tema antigo

O tema WordPress que este projeto substitui tinha alguns recursos que não foram portados:

- **Comentários** — descontinuados por decisão de produto.
- **Anúncios (AdSense)** — o tema tinha quatro posições. Não estão aqui.
- **AMP, post formats e página de biografia** — não portados.
4 changes: 0 additions & 4 deletions app/Http/Middleware/HandleInertiaRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use App\Services\WpAuthService;
use App\Services\WpMenuService;
use Illuminate\Foundation\Inspiring;
use Illuminate\Http\Request;
use Inertia\Middleware;
use Tighten\Ziggy\Ziggy;
Expand Down Expand Up @@ -39,14 +38,11 @@ public function version(Request $request): ?string
*/
public function share(Request $request): array
{
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');

$wpUser = WpAuthService::getLoggedInWpUser();

return [
...parent::share($request),
'name' => config('app.name'),
'quote' => ['message' => trim($message), 'author' => trim($author)],
'auth' => [
'user' => $request->user(),
],
Expand Down
10 changes: 8 additions & 2 deletions app/Models/WpPost.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,14 @@ public function useSearchTerm(Builder $query, ?string $searchTerm = null): void
return;
}

$query->where('post_title', 'LIKE', "%{$searchTerm}%")
->orWhere('post_content', 'LIKE', "%{$searchTerm}%");
// Grouped explicitly. Laravel currently nests the wheres a named scope
// adds, so the OR does not leak today — but that is a property of how
// the scope is invoked, not of this code, and it breaks the moment the
// condition is inlined into a query.
$query->where(function (Builder $query) use ($searchTerm) {
$query->where('post_title', 'LIKE', "%{$searchTerm}%")
->orWhere('post_content', 'LIKE', "%{$searchTerm}%");
});
}

#[Scope]
Expand Down
6 changes: 5 additions & 1 deletion app/Services/WpCategoryService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ public static function getCategoryBySlug($slug)

return cache()->remember($cacheKey, $cacheTTL, function () use ($slug) {
return WpTerm::query()
->where('slug', $slug)
// Slugs are only unique within a taxonomy: without this filter a
// tag sharing a category's slug could be returned instead.
->join('wp_term_taxonomy', 'wp_term_taxonomy.term_id', '=', 'wp_terms.term_id')
->where('wp_term_taxonomy.taxonomy', 'category')
->where('wp_terms.slug', $slug)
->first();
});
}
Expand Down
6 changes: 6 additions & 0 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use App\Http\Middleware\HandleAppearance;
use App\Http\Middleware\HandleInertiaRequests;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
Expand All @@ -14,6 +15,11 @@
)
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
// Reads the appearance cookie the theme switcher writes and shares
// it with the root view. It existed but was never registered, so
// the cookie was written and never read, and anyone who picked a
// theme explicitly got a flash of the wrong one on every full load.
HandleAppearance::class,
HandleInertiaRequests::class,
AddLinkHeadersForPreloadedAssets::class,
]);
Expand Down
7 changes: 5 additions & 2 deletions config/inertia.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
|
*/

// Off by default: when this is true and no SSR process is listening, every
// request pays for a failed connection to 127.0.0.1:13714 before falling
// back to client rendering. `composer dev` does not start that process.
'ssr' => [
'enabled' => true,
'url' => 'http://127.0.0.1:13714',
'enabled' => env('INERTIA_SSR_ENABLED', false),
'url' => env('INERTIA_SSR_URL', 'http://127.0.0.1:13714'),
// 'bundle' => base_path('bootstrap/ssr/ssr.mjs'),
],

Expand Down
3 changes: 2 additions & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_DATABASE" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>
Expand Down
18 changes: 10 additions & 8 deletions resources/js/components/Carousel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ watchOnce(api, (api) => {
:opts="{
loop: true,
}"
:plugins="[Autoplay({
delay: 5000,
active: true,
stopOnFocusIn: false,
stopOnInteraction: false,
stopOnMouseEnter: false,
})]"
:plugins="[
Autoplay({
delay: 5000,
active: true,
stopOnFocusIn: false,
stopOnInteraction: false,
stopOnMouseEnter: false,
}),
]"
>
<CarouselContent>
<CarouselItem v-for="(post, idx) in highlightedPosts.data" :key="post.id" class="relative">
Expand All @@ -62,7 +64,7 @@ watchOnce(api, (api) => {
</div>
<div class="mt-5 mb-0 flex justify-center xl:my-10">
<button
v-for="i in 5"
v-for="i in totalCount"
v-bind:key="i"
class="mx-2 h-[15px] w-[15px] cursor-pointer p-1"
:class="current === i ? 'opacity-100' : 'opacity-50'"
Expand Down
12 changes: 0 additions & 12 deletions resources/js/components/HeaderSearchForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,6 @@ const handleSubmit = () => {
});
};

const handleSearchBlur = () => {
if (!formRef.value) return;
if (!inputRef.value) return;

const termOnInput = inputRef.value.value as string;

if (!searchTerm.value || searchTerm.value !== termOnInput) {
handleSubmit();
}
};

watch(
() => searchOpen,
() => {
Expand Down Expand Up @@ -67,7 +56,6 @@ defineExpose({
class="slide-search-transition absolute right-[42px] h-[42px] rounded-tl-xs rounded-bl-xs border-0 bg-white px-4 py-2 text-black outline-0"
:class="{ 'w-0! p-0!': !searchOpen }"
placeholder="Buscar..."
@blur="handleSearchBlur"
/>
</form>
</template>
Expand Down
11 changes: 10 additions & 1 deletion resources/js/components/Sidebar.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
<script setup lang="ts"></script>

<template>
<div class="h-[500px] w-full bg-gray-800"></div>
<!--
Intentionally empty.

This used to render a 500px grey block (bg-gray-800) on every desktop
page: a placeholder that shipped to production. Rendering nothing reads
better than rendering a fake panel while the column has no content.

The WordPress theme this project replaces filled this column with an ad
unit; whether that comes back is the site owner's call.
-->
</template>

<style scoped></style>
6 changes: 4 additions & 2 deletions resources/js/composables/useAppearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ export function initializeTheme() {
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
}

export function useAppearance() {
const appearance = ref<Appearance>(defaultTheme);
// Module scope on purpose: the header renders a ThemeButton for desktop and
// another for mobile, and a ref created per call would let their icons disagree.
const appearance = ref<Appearance>(defaultTheme);

export function useAppearance() {
onMounted(() => {
const savedAppearance = localStorage.getItem('appearance') as Appearance | null;

Expand Down
19 changes: 9 additions & 10 deletions resources/js/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export interface WpAdmin {

export type AppPageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
name: string;
quote: { message: string; author: string };
auth: Auth;
ziggy: Config & { location: string };
wpAdmin: WpAdmin | null;
Expand All @@ -40,20 +39,20 @@ export interface User {
export interface LaravelPagination<T> {
data: T;
links: {
first: string|null;
last: string|null;
prev: string|null;
},
first: string | null;
last: string | null;
prev: string | null;
};
meta: {
current_page: number;
from: number|null;
from: number | null;
last_page: number;
links: { url: string|null; label: string; active: boolean }[];
links: { url: string | null; label: string; active: boolean }[];
path: string;
per_page: number;
to: number|null;
to: number | null;
total: number;
}
};
}

export interface WpMenuItem {
Expand Down Expand Up @@ -98,7 +97,7 @@ export interface Post {
day: string;
hour?: string;
minute?: string;
},
};
content?: string;
subtitle?: string;
author_description?: string;
Expand Down
7 changes: 0 additions & 7 deletions tests/Feature/ExampleTest.php

This file was deleted.

Loading