diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8af0d58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +*.pyc +.env +.venv/ +.idea/ +.vscode/ +.DS_Store +node_modules/ +.coverage +htmlcov/ +pytest_cache/ diff --git a/README.md b/README.md index aa50a81..b68343b 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,131 @@ # Sensasiwangi.id MVP Workspace -Repositori ini memuat dokumen awal pengembangan untuk proyek **Sensasiwangi.id**, platform wewangian lokal yang dijelaskan pada dokumen [PRD_MVP.md](PRD_MVP.md). +Repositori ini memuat dokumen dan kode awal pengembangan untuk proyek +**Sensasiwangi.id**, platform wewangian lokal yang dijelaskan pada dokumen +[PRD_MVP.md](PRD_MVP.md). + +## Arsitektur Aplikasi + +MVP dibangun menggunakan FastAPI dengan rendering server-side (SSR) melalui +Jinja2 dan integrasi HTMX untuk interaksi progresif sesuai PRD. Struktur modul +utama: + +``` +src/app/ +├── api/ # Router HTTP dan handler tampilan +├── core/ # Konfigurasi aplikasi & factory +├── services/ # Tempat logika domain (akan diisi bertahap) +└── web/ + ├── static/ # Asset glassmorphism (CSS, ikon, dsb.) + └── templates/ # Template Jinja2 + partial navbar/footer +``` + +`create_app` pada `src/app/core/application.py` menyiapkan middleware, static +files, dan registrasi router untuk halaman landing awal. + +## Menjalankan Secara Lokal + +1. Buat virtualenv dan instal dependensi: + + ```bash + python -m venv .venv + source .venv/bin/activate + pip install -e .[dev] + ``` + +2. Jalankan server pengembangan dengan Uvicorn: + + ```bash + uvicorn app.main:app --reload + ``` + +3. Akses `http://localhost:8000` untuk melihat landing page awal bertema +glassmorphism. + +## Pengujian + +Gunakan `pytest` untuk menjalankan test dasar yang memverifikasi homepage SSR: + +```bash +pytest +``` ## To-Do List Implementasi Berdasarkan PRD -Daftar berikut dirinci mengikuti fase prioritas dan milestone yang disebutkan di PRD. Checklist dapat disesuaikan selama sprint planning, namun urutan menjaga dependensi utama. + +Daftar berikut dirinci mengikuti fase prioritas dan milestone yang disebutkan di +PRD. Checklist dapat disesuaikan selama sprint planning, namun urutan menjaga +dependensi utama. ### Fase Fondasi Teknis (Minggu 1-2) -- [ ] Finalisasi skema data inti di Supabase untuk modul: pesanan, produk (termasuk atribut Sambatan), brand/etalase, artikel Nusantarum, profil pengguna, dan relasi user-brand. -- [ ] Provisioning proyek Supabase (staging & production), termasuk konfigurasi Auth (magic link), Storage bucket per brand/artikel, dan variabel lingkungan (`SUPABASE_URL`, `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`). -- [ ] Menyiapkan repositori kode backend (FastAPI + HTMX/Jinja2) dengan struktur modul sesuai layanan yang dibutuhkan (OrderService, ProductService, SambatanLifecycle, dsb.). -- [ ] Menyiapkan panduan desain awal: wireframe navigasi utama (Dashboard, Marketplace, Nusantarum, Profil) dan style tile bertema glassmorphism (rujuk `docs/guideline-glassmorphism.md` ketika tersedia). -- [ ] Menentukan SOP verifikasi merchant pilot & kurasi konten awal sebagai input onboarding. +- [ ] Finalisasi skema data inti di Supabase untuk modul: pesanan, produk + (termasuk atribut Sambatan), brand/etalase, artikel Nusantarum, profil + pengguna, dan relasi user-brand. +- [ ] Provisioning proyek Supabase (staging & production), termasuk konfigurasi + Auth (magic link), Storage bucket per brand/artikel, dan variabel + lingkungan (`SUPABASE_URL`, `SUPABASE_ANON_KEY`, + `SUPABASE_SERVICE_ROLE_KEY`). +- [ ] Menyiapkan repositori kode backend (FastAPI + HTMX/Jinja2) dengan struktur + modul sesuai layanan yang dibutuhkan (OrderService, ProductService, + SambatanLifecycle, dsb.). +- [ ] Menyiapkan panduan desain awal: wireframe navigasi utama (Dashboard, + Marketplace, Nusantarum, Profil) dan style tile bertema glassmorphism + (rujuk `docs/guideline-glassmorphism.md` ketika tersedia). +- [ ] Menentukan SOP verifikasi merchant pilot & kurasi konten awal sebagai + input onboarding. ### Fase Pesanan & Operasional (Minggu 3-4) -- [ ] Implementasi alur pembuatan pesanan: form produk, kuantitas, alamat RajaOngkir, jadwal pengiriman, dan status awal `Draf`. -- [ ] Membangun dashboard operasional internal untuk memperbarui status pesanan, menambahkan nomor resi, dan mencatat audit trail. +- [ ] Implementasi alur pembuatan pesanan: form produk, kuantitas, alamat + RajaOngkir, jadwal pengiriman, dan status awal `Draf`. +- [ ] Membangun dashboard operasional internal untuk memperbarui status pesanan, + menambahkan nomor resi, dan mencatat audit trail. - [ ] Implementasi ekspor CSV laporan pesanan harian. -- [ ] Membuat profil brand internal (form data brand, sertifikasi, kontak) serta relasi user-brand (owner/admin/kontributor). -- [ ] Integrasi API RajaOngkir untuk validasi alamat dan estimasi ongkir saat checkout internal. +- [ ] Membuat profil brand internal (form data brand, sertifikasi, kontak) serta + relasi user-brand (owner/admin/kontributor). +- [ ] Integrasi API RajaOngkir untuk validasi alamat dan estimasi ongkir saat + checkout internal. ### Fase Marketplace & Brand Publik (Minggu 5) -- [ ] Mengimplementasikan halaman marketplace dengan empat tab kategori (Parfum, Raw Material, Tools, Lainnya), pencarian teks per tab, dan filter aroma tambahan. -- [ ] Menampilkan detail produk minimum (nama, harga indikatif, stok, foto utama, highlight aroma) dengan dukungan mode Sambatan. -- [ ] Menyelesaikan profil brand publik: deskripsi singkat, katalog produk, story Nusantara, sertifikasi, tautan artikel Nusantarum, dan CTA kontak. -- [ ] Menyiapkan grid listing Sambatan beserta indikator progress bar dan countdown di halaman produk. +- [ ] Mengimplementasikan halaman marketplace dengan empat tab kategori + (Parfum, Raw Material, Tools, Lainnya), pencarian teks per tab, dan filter + aroma tambahan. +- [ ] Menampilkan detail produk minimum (nama, harga indikatif, stok, foto + utama, highlight aroma) dengan dukungan mode Sambatan. +- [ ] Menyelesaikan profil brand publik: deskripsi singkat, katalog produk, + story Nusantara, sertifikasi, tautan artikel Nusantarum, dan CTA kontak. +- [ ] Menyiapkan grid listing Sambatan beserta indikator progress bar dan + countdown di halaman produk. ### Fase Nusantarum & Konten (Minggu 6) -- [ ] Mengembangkan halaman Nusantarum dengan highlight hero dan tiga tab (Parfum, Brand, Perfumer) plus panel filter kaca (kategori aroma, wilayah, kurator, status linked) dan pencarian teks. -- [ ] Membangun panel manajemen konten kurator untuk menambah entri, menandai status verifikasi, serta melakukan linking ke profil terkait. -- [ ] Menautkan artikel Nusantarum ke produk/brand/perfumer relevan guna mengarahkan traffic. +- [ ] Mengembangkan halaman Nusantarum dengan highlight hero dan tiga tab + (Parfum, Brand, Perfumer) plus panel filter kaca (kategori aroma, wilayah, + kurator, status linked) dan pencarian teks. +- [ ] Membangun panel manajemen konten kurator untuk menambah entri, menandai + status verifikasi, serta melakukan linking ke profil terkait. +- [ ] Menautkan artikel Nusantarum ke produk/brand/perfumer relevan guna + mengarahkan traffic. ### Fase Profil Pengguna & Sambatan (Minggu 7) -- [ ] Membuat profil pengguna marketplace (nama, email, preferensi aroma) dan fitur favorit sederhana. -- [ ] Implementasi alur Sambatan: create request (`is_sambatan`, `total_slots`, `sambatan_deadline`), join request dengan transaksi aman, progress otomatis (`remaining_slots`, `sambatan_progress_percentage`). -- [ ] Mengembangkan `SambatanLifecycleService` & scheduler untuk menangani deadline, payout/refund, serta logging ke `SambatanAuditLog`. -- [ ] Membuat dashboard seller Sambatan dengan statistik slot, kontribusi terakhir, dan kontrol status. +- [ ] Membuat profil pengguna marketplace (nama, email, preferensi aroma) dan + fitur favorit sederhana. +- [ ] Implementasi alur Sambatan: create request (`is_sambatan`, `total_slots`, + `sambatan_deadline`), join request dengan transaksi aman, progress otomatis + (`remaining_slots`, `sambatan_progress_percentage`). +- [ ] Mengembangkan `SambatanLifecycleService` & scheduler untuk menangani + deadline, payout/refund, serta logging ke `SambatanAuditLog`. +- [ ] Membuat dashboard seller Sambatan dengan statistik slot, kontribusi + terakhir, dan kontrol status. ### Fase QA, Peluncuran, & Operasional (Minggu 8) -- [ ] Menjalankan suite QA lintas modul: pytest (async + coverage), linting Ruff & Black, dan verifikasi migrasi Supabase. -- [ ] Melaksanakan UAT internal dengan skenario marketplace, Nusantarum, dan Sambatan (termasuk linking ulang oleh kurator). -- [ ] Menyusun SOP operasional akhir: respon tiket Sambatan, fallback manual refund/payout, dan monitoring log dasar. -- [ ] Menyiapkan deployment ke Vercel atau alternatif kontainer (Gunicorn + Uvicorn) dan strategi rollback. -- [ ] Mengaktifkan monitoring dasar serta menyiapkan checklist post-mortem jika terjadi kegagalan besar. +- [ ] Menjalankan suite QA lintas modul: pytest (async + coverage), linting Ruff + & Black, dan verifikasi migrasi Supabase. +- [ ] Melaksanakan UAT internal dengan skenario marketplace, Nusantarum, dan + Sambatan (termasuk linking ulang oleh kurator). +- [ ] Menyusun SOP operasional akhir: respon tiket Sambatan, fallback manual + refund/payout, dan monitoring log dasar. +- [ ] Menyiapkan deployment ke Vercel atau alternatif kontainer (Gunicorn + + Uvicorn) dan strategi rollback. +- [ ] Mengaktifkan monitoring dasar serta menyiapkan checklist post-mortem jika + terjadi kegagalan besar. ### Backlog Pasca-MVP - [ ] Notifikasi otomatis (email/push) untuk event pesanan dan Sambatan. @@ -50,5 +134,5 @@ Daftar berikut dirinci mengikuti fase prioritas dan milestone yang disebutkan di - [ ] Aplikasi mobile native dan multi-bahasa. ## Referensi -- [PRD Sensasiwangi.id](PRD_MVP.md) +- [PRD Sensasiwangi.id](PRD_MVP.md) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..776af0f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["setuptools>=67", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "sensasiwangi" +version = "0.1.0" +description = "MVP web application for Sensasiwangi.id" +authors = [{ name = "Sensasiwangi Team" }] +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "fastapi>=0.110", + "uvicorn[standard]>=0.27", + "jinja2>=3.1", + "python-multipart>=0.0.6", + "pydantic-settings>=2.2", + "aiofiles>=23.2", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "httpx>=0.27", + "anyio>=4.0", +] + +[tool.black] +line-length = 88 +target-version = ["py311"] + +[tool.ruff] +line-length = 88 +src = ["src", "tests"] +select = ["E", "F", "I", "UP", "B", "A", "COM", "C4"] +ignore = ["E501"] + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +"app" = [ + "web/templates/**/*.html", + "web/static/**/*", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/api/__init__.py b/src/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/api/routes/__init__.py b/src/app/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/api/routes/root.py b/src/app/api/routes/root.py new file mode 100644 index 0000000..8684817 --- /dev/null +++ b/src/app/api/routes/root.py @@ -0,0 +1,22 @@ +"""Landing page routes for the MVP.""" + +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse + +from app.core.config import get_settings + +router = APIRouter() + + +@router.get("/", response_class=HTMLResponse) +async def read_home(request: Request) -> HTMLResponse: + """Render the marketplace landing page placeholder.""" + + settings = get_settings() + templates = request.app.state.templates + context = { + "request": request, + "app_name": settings.app_name, + "environment": settings.environment, + } + return templates.TemplateResponse("index.html", context) diff --git a/src/app/core/__init__.py b/src/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/core/application.py b/src/app/core/application.py new file mode 100644 index 0000000..4874e63 --- /dev/null +++ b/src/app/core/application.py @@ -0,0 +1,41 @@ +"""Application factory for the Sensasiwangi.id MVP.""" + +from pathlib import Path + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from starlette.staticfiles import StaticFiles + +from app.core.config import get_settings +from app.web.templates import template_engine +from app.api.routes import root as root_routes + +STATIC_DIR = Path(__file__).resolve().parent.parent / "web" / "static" + + +def create_app() -> FastAPI: + """Create and configure the FastAPI application instance.""" + + settings = get_settings() + + app = FastAPI(title=settings.app_name) + + # Basic CORS setup to simplify early integrations with Supabase and + # front-end previews during the MVP stage. + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], + ) + + # Mount static assets (CSS, JS, images) served by the Jinja2 templates. + app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") + + # Register routers for server-rendered pages. + app.include_router(root_routes.router) + + # Expose the template engine on the app state for reuse by routers. + app.state.templates = template_engine + + return app diff --git a/src/app/core/config.py b/src/app/core/config.py new file mode 100644 index 0000000..7cdae3c --- /dev/null +++ b/src/app/core/config.py @@ -0,0 +1,33 @@ +"""Application configuration module.""" + +from functools import lru_cache +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Centralized application settings loaded from environment variables. + + The fields reflect integrations highlighted in the PRD such as Supabase and + RajaOngkir. Additional configuration values can be appended as features are + implemented. + """ + + app_name: str = "Sensasiwangi.id" + environment: str = "development" + + # Supabase configuration + supabase_url: str | None = None + supabase_anon_key: str | None = None + supabase_service_role_key: str | None = None + + # RajaOngkir integration + rajaongkir_api_key: str | None = None + + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") + + +@lru_cache +def get_settings() -> Settings: + """Return cached settings instance.""" + + return Settings() diff --git a/src/app/main.py b/src/app/main.py new file mode 100644 index 0000000..a43c66b --- /dev/null +++ b/src/app/main.py @@ -0,0 +1,5 @@ +"""Entry point for ASGI servers.""" + +from app.core.application import create_app + +app = create_app() diff --git a/src/app/services/__init__.py b/src/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/web/static/css/base.css b/src/app/web/static/css/base.css new file mode 100644 index 0000000..ddad470 --- /dev/null +++ b/src/app/web/static/css/base.css @@ -0,0 +1,158 @@ +:root { + --bg-gradient: linear-gradient(135deg, rgba(243, 238, 255, 0.85), rgba(224, 247, 250, 0.85)); + --glass-border: rgba(255, 255, 255, 0.6); + --glass-shadow: 0 20px 40px rgba(15, 23, 42, 0.2); + --primary: #6a4dd6; + --secondary: #00a896; + --text-color: #1f2937; + --muted: #6b7280; + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +body { + min-height: 100vh; + background: var(--bg-gradient); + color: var(--text-color); + display: flex; + justify-content: center; +} + +.page-wrapper { + width: min(1200px, 100%); + padding: 2rem 1.5rem 4rem; + display: flex; + flex-direction: column; + gap: 2rem; +} + +.glass-panel { + backdrop-filter: blur(16px); + background: rgba(255, 255, 255, 0.55); + border-radius: 24px; + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow); +} + +.navbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + position: sticky; + top: 1rem; + z-index: 10; +} + +.navbar-brand .logo { + font-weight: 700; + letter-spacing: 0.04em; +} + +.navbar-menu { + list-style: none; + display: flex; + gap: 1.5rem; +} + +.navbar-menu a { + text-decoration: none; + color: var(--muted); + font-weight: 500; +} + +.navbar-menu a:hover { + color: var(--text-color); +} + +.btn { + border-radius: 999px; + padding: 0.75rem 1.5rem; + text-decoration: none; + font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.btn.primary { + background: var(--primary); + color: white; +} + +.btn.secondary { + background: transparent; + border: 1px solid var(--primary); + color: var(--primary); +} + +.btn.tertiary { + background: transparent; + border: 1px solid transparent; + color: var(--text-color); +} + +.hero { + display: grid; + gap: 2rem; + padding: 3rem; + align-items: center; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); +} + +.hero-text h1 { + font-size: clamp(2rem, 3vw, 3rem); + margin-bottom: 1rem; +} + +.hero-text p { + color: var(--muted); + line-height: 1.6; + margin-bottom: 1.5rem; +} + +.hero-visual { + min-height: 220px; + border-radius: 24px; + background: linear-gradient(160deg, rgba(106, 77, 214, 0.4), rgba(0, 168, 150, 0.4)); + backdrop-filter: blur(20px); +} + +.section { + padding: 2rem 2.5rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.footer { + background: linear-gradient(135deg, rgba(106, 77, 214, 0.85), rgba(0, 168, 150, 0.85)); + border-radius: 24px; + padding: 1.5rem 2rem; + color: white; +} + +.footer-links { + display: flex; + gap: 1rem; + list-style: none; +} + +.footer-links a { + color: white; + text-decoration: none; +} + +@media (max-width: 768px) { + .navbar-menu { + display: none; + } + + .page-wrapper { + padding: 1.5rem 1rem 3rem; + } + + .hero { + padding: 2rem; + } +} diff --git a/src/app/web/templates/__init__.py b/src/app/web/templates/__init__.py new file mode 100644 index 0000000..c882903 --- /dev/null +++ b/src/app/web/templates/__init__.py @@ -0,0 +1,8 @@ +"""Template utilities for Jinja2 rendering.""" + +from pathlib import Path +from fastapi.templating import Jinja2Templates + +TEMPLATES_DIR = Path(__file__).parent + +template_engine = Jinja2Templates(directory=str(TEMPLATES_DIR)) diff --git a/src/app/web/templates/base.html b/src/app/web/templates/base.html new file mode 100644 index 0000000..e840b83 --- /dev/null +++ b/src/app/web/templates/base.html @@ -0,0 +1,20 @@ + + +
+ + ++ Platform wewangian lokal dengan marketplace terkurasi, kanal editorial + Nusantarum, dan fitur Sambatan untuk kolaborasi produksi. +
+ ++ Mulai bangun struktur katalog empat kategori (Parfum, Raw Material, Tools, + Lainnya). Komponen ini akan terhubung dengan Supabase untuk data produk + setelah skema siap. +
++ Artikel kurasi akan tampil di sini lengkap dengan filter aroma, wilayah, dan + kurator. Struktur tab dan panel filter akan diimplementasikan pada sprint + konten. +
++ Progress bar, slot partisipasi, dan countdown deadline akan hadir untuk + mendukung kampanye kolaborasi produksi sesuai PRD. +
+