feat: phase 3 website pages on v1 api
This commit is contained in:
@@ -14,6 +14,7 @@ This documentation now uses the corrected legacy/source folders:
|
||||
4. `rebuild-progress.md`
|
||||
5. `phase-1-foundation.md`
|
||||
6. `phase-2-api.md`
|
||||
7. `phase-3-website.md`
|
||||
|
||||
## Note
|
||||
|
||||
|
||||
71
app/app/Http/Controllers/Web/SiteController.php
Normal file
71
app/app/Http/Controllers/Web/SiteController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
public function home(): View
|
||||
{
|
||||
return view('site.home');
|
||||
}
|
||||
|
||||
public function apiDocs(): View
|
||||
{
|
||||
return view('site.api-docs');
|
||||
}
|
||||
|
||||
public function pricing(): View
|
||||
{
|
||||
return view('site.pricing');
|
||||
}
|
||||
|
||||
public function privacy(): View
|
||||
{
|
||||
return view('site.privacy');
|
||||
}
|
||||
|
||||
public function terms(): View
|
||||
{
|
||||
return view('site.terms');
|
||||
}
|
||||
|
||||
public function emojiDetail(string $slug): View|Response
|
||||
{
|
||||
$dataPath = (string) config('dewemoji.data_path');
|
||||
if (!is_file($dataPath)) {
|
||||
abort(500, 'Emoji dataset file not found.');
|
||||
}
|
||||
|
||||
$raw = file_get_contents($dataPath);
|
||||
if ($raw === false) {
|
||||
abort(500, 'Emoji dataset file could not be read.');
|
||||
}
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
abort(500, 'Emoji dataset JSON is invalid.');
|
||||
}
|
||||
|
||||
$items = $decoded['emojis'] ?? [];
|
||||
$match = null;
|
||||
foreach ($items as $item) {
|
||||
if (($item['slug'] ?? '') === $slug) {
|
||||
$match = $item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$match) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('site.emoji-detail', [
|
||||
'emoji' => $match,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
49
app/resources/views/site/api-docs.blade.php
Normal file
49
app/resources/views/site/api-docs.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
@extends('site.layout')
|
||||
|
||||
@section('title', 'API Docs - Dewemoji')
|
||||
|
||||
@section('content')
|
||||
<section class="card" style="padding: 20px;">
|
||||
<h1 style="margin-top:0;">API Docs</h1>
|
||||
<p style="color:var(--muted);">Current extension-compatible endpoints exposed by the rebuild app.</p>
|
||||
|
||||
<h2>Base URL</h2>
|
||||
<p><code>{{ url('/') }}/v1</code></p>
|
||||
|
||||
<h2>Endpoints</h2>
|
||||
<ul>
|
||||
<li><code>GET /v1/categories</code> - category + subcategory map</li>
|
||||
<li><code>GET /v1/emojis</code> - paginated emoji list/search</li>
|
||||
<li><code>POST /v1/license/verify</code> - license validation contract</li>
|
||||
</ul>
|
||||
|
||||
<h2>Example: emojis</h2>
|
||||
<pre class="card" style="padding:12px;overflow:auto;">GET /v1/emojis?q=love&category=Smileys%20%26%20Emotion&page=1&limit=20</pre>
|
||||
|
||||
<h2>Example response</h2>
|
||||
<pre class="card" style="padding:12px;overflow:auto;">{
|
||||
"items": [
|
||||
{
|
||||
"emoji": "😀",
|
||||
"name": "grinning face",
|
||||
"slug": "grinning-face",
|
||||
"category": "Smileys & Emotion",
|
||||
"subcategory": "face-smiling",
|
||||
"supports_skin_tone": false,
|
||||
"summary": "A happy smiling face."
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"limit": 20
|
||||
}</pre>
|
||||
|
||||
<h2>Try it quickly</h2>
|
||||
<p>
|
||||
<a href="{{ url('/v1/categories') }}" target="_blank" style="color:var(--brand);">Open categories JSON</a>
|
||||
·
|
||||
<a href="{{ url('/v1/emojis?q=love&limit=5&page=1') }}" target="_blank" style="color:var(--brand);">Open emojis JSON</a>
|
||||
</p>
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
53
app/resources/views/site/emoji-detail.blade.php
Normal file
53
app/resources/views/site/emoji-detail.blade.php
Normal file
@@ -0,0 +1,53 @@
|
||||
@extends('site.layout')
|
||||
|
||||
@section('title', ($emoji['name'] ?? 'Emoji').' - Dewemoji')
|
||||
|
||||
@section('content')
|
||||
<article class="card" style="padding: 20px;">
|
||||
<p style="margin-top:0;"><a href="{{ route('home') }}" style="color: var(--brand); text-decoration:none;">← Back to emoji list</a></p>
|
||||
|
||||
<div style="display:grid; grid-template-columns: 120px 1fr; gap: 18px; align-items: start;">
|
||||
<div style="font-size: 82px; line-height: 1;">{{ $emoji['emoji'] ?? '' }}</div>
|
||||
<div>
|
||||
<h1 style="margin:0 0 8px 0;">{{ $emoji['name'] ?? '' }}</h1>
|
||||
<p style="margin:0;color:var(--muted);">{{ $emoji['category'] ?? '' }} / {{ $emoji['subcategory'] ?? '' }}</p>
|
||||
<p style="margin:12px 0 0 0; color:var(--muted);">{{ $emoji['description'] ?? '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr style="border:none;border-top:1px solid var(--border);margin:18px 0;">
|
||||
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
|
||||
<code>Slug: {{ $emoji['slug'] ?? '' }}</code>
|
||||
@if(!empty($emoji['unified']))
|
||||
<code>Unified: {{ $emoji['unified'] }}</code>
|
||||
@endif
|
||||
<button id="copy-btn" style="border:1px solid var(--border);background:white;border-radius:10px;padding:8px 12px;cursor:pointer;">
|
||||
Copy Emoji
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if(!empty($emoji['keywords_en']) && is_array($emoji['keywords_en']))
|
||||
<h3 style="margin-top: 20px;">Keywords (EN)</h3>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px;">
|
||||
@foreach($emoji['keywords_en'] as $kw)
|
||||
<code>{{ $kw }}</code>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</article>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.getElementById('copy-btn')?.addEventListener('click', async () => {
|
||||
const emoji = @json($emoji['emoji'] ?? '');
|
||||
await navigator.clipboard.writeText(emoji);
|
||||
const btn = document.getElementById('copy-btn');
|
||||
const old = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => btn.textContent = old, 1200);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
169
app/resources/views/site/home.blade.php
Normal file
169
app/resources/views/site/home.blade.php
Normal file
@@ -0,0 +1,169 @@
|
||||
@extends('site.layout')
|
||||
|
||||
@section('title', 'Dewemoji - Emoji Browser')
|
||||
|
||||
@section('content')
|
||||
<section class="card" style="padding: 18px;">
|
||||
<h1 style="margin: 0 0 6px 0;">Emoji Browser</h1>
|
||||
<p style="margin: 0; color: var(--muted);">Rebuilt website powered by <code>/v1</code> APIs.</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 10px; margin-top: 16px;" id="filters">
|
||||
<input id="q" type="text" placeholder="Search (e.g. love, senyum)" style="padding: 10px; border: 1px solid var(--border); border-radius: 10px;">
|
||||
<select id="category" style="padding: 10px; border: 1px solid var(--border); border-radius: 10px;">
|
||||
<option value="">All categories</option>
|
||||
</select>
|
||||
<select id="subcategory" style="padding: 10px; border: 1px solid var(--border); border-radius: 10px;" disabled>
|
||||
<option value="">All subcategories</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" style="margin-top: 14px; padding: 14px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||||
<strong>Results</strong>
|
||||
<span id="count" style="color: var(--muted); font-size: 13px;">0 / 0</span>
|
||||
</div>
|
||||
|
||||
<div id="grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(94px, 1fr)); gap: 10px;"></div>
|
||||
|
||||
<div style="text-align: center; margin-top: 14px;">
|
||||
<button id="more" style="display:none; border: 1px solid var(--border); background: white; border-radius: 10px; padding: 10px 14px; cursor: pointer;">
|
||||
Load more
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
(() => {
|
||||
const state = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 0,
|
||||
items: [],
|
||||
categories: {},
|
||||
};
|
||||
|
||||
const qEl = document.getElementById('q');
|
||||
const catEl = document.getElementById('category');
|
||||
const subEl = document.getElementById('subcategory');
|
||||
const grid = document.getElementById('grid');
|
||||
const count = document.getElementById('count');
|
||||
const more = document.getElementById('more');
|
||||
|
||||
function esc(s) {
|
||||
return String(s || '').replace(/[&<>"']/g, (c) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[c]));
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
const res = await fetch('/v1/categories');
|
||||
const data = await res.json();
|
||||
state.categories = data || {};
|
||||
Object.keys(state.categories).forEach((name) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = name;
|
||||
opt.textContent = name;
|
||||
catEl.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function renderSubcategories() {
|
||||
const category = catEl.value;
|
||||
const subs = state.categories[category] || [];
|
||||
subEl.innerHTML = '<option value="">All subcategories</option>';
|
||||
subs.forEach((s) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s;
|
||||
opt.textContent = s;
|
||||
subEl.appendChild(opt);
|
||||
});
|
||||
subEl.disabled = subs.length === 0;
|
||||
}
|
||||
|
||||
async function fetchEmojis(reset = false) {
|
||||
if (reset) {
|
||||
state.page = 1;
|
||||
state.items = [];
|
||||
grid.innerHTML = '';
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: String(state.page),
|
||||
limit: String(state.limit),
|
||||
});
|
||||
|
||||
if (qEl.value.trim()) params.set('q', qEl.value.trim());
|
||||
if (catEl.value) params.set('category', catEl.value);
|
||||
if (subEl.value) params.set('subcategory', subEl.value);
|
||||
|
||||
const res = await fetch('/v1/emojis?' + params.toString());
|
||||
const data = await res.json();
|
||||
state.total = data.total || 0;
|
||||
|
||||
(data.items || []).forEach((item) => state.items.push(item));
|
||||
renderGrid(data.items || [], reset);
|
||||
updateCount();
|
||||
updateMoreButton();
|
||||
}
|
||||
|
||||
function renderGrid(items, reset) {
|
||||
if (reset && items.length === 0) {
|
||||
grid.innerHTML = '<p style="color:var(--muted);grid-column:1/-1;">No emojis found.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach((item) => {
|
||||
const card = document.createElement('a');
|
||||
card.href = '/emoji/' + encodeURIComponent(item.slug);
|
||||
card.className = 'card';
|
||||
card.style.cssText = 'padding:10px;text-decoration:none;color:inherit;display:flex;flex-direction:column;align-items:center;gap:6px;';
|
||||
card.innerHTML = `
|
||||
<div style="font-size:30px;">${esc(item.emoji)}</div>
|
||||
<div style="font-size:12px;text-align:center;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:100%">${esc(item.name)}</div>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCount() {
|
||||
count.textContent = `${state.items.length} / ${state.total}`;
|
||||
}
|
||||
|
||||
function updateMoreButton() {
|
||||
const canLoad = state.items.length < state.total;
|
||||
more.style.display = canLoad ? 'inline-block' : 'none';
|
||||
}
|
||||
|
||||
let timer = null;
|
||||
qEl.addEventListener('input', () => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fetchEmojis(true), 220);
|
||||
});
|
||||
|
||||
catEl.addEventListener('change', async () => {
|
||||
renderSubcategories();
|
||||
await fetchEmojis(true);
|
||||
});
|
||||
|
||||
subEl.addEventListener('change', () => fetchEmojis(true));
|
||||
|
||||
more.addEventListener('click', async () => {
|
||||
state.page += 1;
|
||||
await fetchEmojis(false);
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await loadCategories();
|
||||
await fetchEmojis(true);
|
||||
})();
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
92
app/resources/views/site/layout.blade.php
Normal file
92
app/resources/views/site/layout.blade.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>@yield('title', 'Dewemoji')</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f7fafc;
|
||||
--surface: #ffffff;
|
||||
--fg: #0f172a;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--brand: #2563eb;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: var(--fg);
|
||||
background: radial-gradient(circle at 10% 0%, #dbeafe, #f8fafc 32%, #f8fafc 100%);
|
||||
}
|
||||
.wrap { max-width: 1080px; margin: 0 auto; padding: 20px; }
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(8px);
|
||||
background: rgba(248, 250, 252, 0.82);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.topbar .row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.brand { font-weight: 700; text-decoration: none; color: var(--fg); }
|
||||
.nav { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
.nav a { color: var(--muted); text-decoration: none; font-size: 14px; }
|
||||
.nav a:hover { color: var(--brand); }
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
.footer {
|
||||
margin-top: 36px;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
code {
|
||||
background: #eef2ff;
|
||||
color: #1e3a8a;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.wrap { padding: 14px; }
|
||||
.topbar .row { flex-direction: column; align-items: flex-start; }
|
||||
}
|
||||
</style>
|
||||
@stack('head')
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="wrap row">
|
||||
<a class="brand" href="{{ route('home') }}">Dewemoji</a>
|
||||
<nav class="nav">
|
||||
<a href="{{ route('home') }}">Emoji</a>
|
||||
<a href="{{ route('api-docs') }}">API Docs</a>
|
||||
<a href="{{ route('pricing') }}">Pricing</a>
|
||||
<a href="{{ route('privacy') }}">Privacy</a>
|
||||
<a href="{{ route('terms') }}">Terms</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="wrap">
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
<footer class="footer wrap">
|
||||
<p>© {{ now()->year }} Dewemoji.</p>
|
||||
</footer>
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
11
app/resources/views/site/pricing.blade.php
Normal file
11
app/resources/views/site/pricing.blade.php
Normal file
@@ -0,0 +1,11 @@
|
||||
@extends('site.layout')
|
||||
|
||||
@section('title', 'Pricing - Dewemoji')
|
||||
|
||||
@section('content')
|
||||
<section class="card" style="padding: 20px;">
|
||||
<h1 style="margin-top:0;">Pricing</h1>
|
||||
<p style="color:var(--muted);">Phase 3 placeholder page. We will wire real pricing content and purchase flow in a later phase.</p>
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
11
app/resources/views/site/privacy.blade.php
Normal file
11
app/resources/views/site/privacy.blade.php
Normal file
@@ -0,0 +1,11 @@
|
||||
@extends('site.layout')
|
||||
|
||||
@section('title', 'Privacy - Dewemoji')
|
||||
|
||||
@section('content')
|
||||
<section class="card" style="padding: 20px;">
|
||||
<h1 style="margin-top:0;">Privacy</h1>
|
||||
<p style="color:var(--muted);">Phase 3 placeholder page. We will migrate the full privacy text from legacy content in a later pass.</p>
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
11
app/resources/views/site/terms.blade.php
Normal file
11
app/resources/views/site/terms.blade.php
Normal file
@@ -0,0 +1,11 @@
|
||||
@extends('site.layout')
|
||||
|
||||
@section('title', 'Terms - Dewemoji')
|
||||
|
||||
@section('content')
|
||||
<section class="card" style="padding: 20px;">
|
||||
<h1 style="margin-top:0;">Terms</h1>
|
||||
<p style="color:var(--muted);">Phase 3 placeholder page. We will migrate complete terms content from legacy sources in a later pass.</p>
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Web\SiteController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
});
|
||||
Route::get('/', [SiteController::class, 'home'])->name('home');
|
||||
Route::get('/api-docs', [SiteController::class, 'apiDocs'])->name('api-docs');
|
||||
Route::get('/emoji/{slug}', [SiteController::class, 'emojiDetail'])->name('emoji-detail');
|
||||
|
||||
Route::get('/pricing', [SiteController::class, 'pricing'])->name('pricing');
|
||||
Route::get('/privacy', [SiteController::class, 'privacy'])->name('privacy');
|
||||
Route::get('/terms', [SiteController::class, 'terms'])->name('terms');
|
||||
|
||||
37
app/tests/Feature/SitePagesTest.php
Normal file
37
app/tests/Feature/SitePagesTest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class SitePagesTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config()->set('dewemoji.data_path', base_path('tests/Fixtures/emojis.fixture.json'));
|
||||
}
|
||||
|
||||
public function test_core_pages_are_available(): void
|
||||
{
|
||||
$this->get('/')->assertOk();
|
||||
$this->get('/api-docs')->assertOk();
|
||||
$this->get('/pricing')->assertOk();
|
||||
$this->get('/privacy')->assertOk();
|
||||
$this->get('/terms')->assertOk();
|
||||
}
|
||||
|
||||
public function test_emoji_detail_page_works_with_valid_slug(): void
|
||||
{
|
||||
$this->get('/emoji/grinning-face')
|
||||
->assertOk()
|
||||
->assertSee('grinning face');
|
||||
}
|
||||
|
||||
public function test_emoji_detail_page_returns_404_for_unknown_slug(): void
|
||||
{
|
||||
$this->get('/emoji/unknown-slug')->assertNotFound();
|
||||
}
|
||||
}
|
||||
|
||||
42
phase-3-website.md
Normal file
42
phase-3-website.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Phase 3 Website Delivery
|
||||
|
||||
## Implemented routes
|
||||
|
||||
In `app/routes/web.php`:
|
||||
|
||||
- `GET /` (home)
|
||||
- `GET /emoji/{slug}` (detail)
|
||||
- `GET /api-docs`
|
||||
- `GET /pricing`
|
||||
- `GET /privacy`
|
||||
- `GET /terms`
|
||||
|
||||
## Implemented pages
|
||||
|
||||
In `app/resources/views/site/`:
|
||||
|
||||
- `layout.blade.php`
|
||||
- `home.blade.php`
|
||||
- `emoji-detail.blade.php`
|
||||
- `api-docs.blade.php`
|
||||
- `pricing.blade.php`
|
||||
- `privacy.blade.php`
|
||||
- `terms.blade.php`
|
||||
|
||||
## Behavior
|
||||
|
||||
- Home page fetches categories and emojis from the new APIs:
|
||||
- `/v1/categories`
|
||||
- `/v1/emojis`
|
||||
- Supports search/category/subcategory filtering and pagination via "Load more".
|
||||
- Emoji cards link to server-rendered detail page by slug.
|
||||
|
||||
## Controller
|
||||
|
||||
- `app/app/Http/Controllers/Web/SiteController.php`
|
||||
- Handles page rendering and slug-based emoji lookup from configured dataset.
|
||||
|
||||
## Test coverage
|
||||
|
||||
- `app/tests/Feature/SitePagesTest.php`
|
||||
- Validates core pages, valid emoji detail, and 404 for invalid slug.
|
||||
@@ -34,7 +34,7 @@
|
||||
- [x] Support both `q` and `query` inputs.
|
||||
|
||||
### Phase 3 - Website rebuild
|
||||
- [ ] Build website pages in new app (index, emoji detail, api docs, legal pages).
|
||||
- [x] Build website pages in new app (index, emoji detail, api docs, legal pages).
|
||||
- [ ] Replace scaffold in `dewemoji-site` via new NativePHP output.
|
||||
|
||||
### Phase 4 - Extension integration
|
||||
@@ -57,3 +57,10 @@
|
||||
- Routes are now available at `/v1/*` (no `/api` prefix) for extension compatibility.
|
||||
- License verification is currently environment-driven (`DEWEMOJI_LICENSE_ACCEPT_ALL` / `DEWEMOJI_PRO_KEYS`) as a safe stub before real provider integration.
|
||||
- Test coverage added for `v1` endpoints in `app/tests/Feature/ApiV1EndpointsTest.php`.
|
||||
|
||||
## Implementation notes (Phase 3)
|
||||
|
||||
- Added website routes/pages in Laravel app:
|
||||
- `/`, `/emoji/{slug}`, `/api-docs`, `/pricing`, `/privacy`, `/terms`
|
||||
- Home page now consumes `/v1/categories` and `/v1/emojis` directly.
|
||||
- Added page tests in `app/tests/Feature/SitePagesTest.php`.
|
||||
|
||||
Reference in New Issue
Block a user