diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e6d0709..160d817 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,7 +12,7 @@ jobs:
release:
uses: saucebase-dev/saucebase/.github/workflows/semantic-release.yml@main
with:
- version_file: 'module.json'
+ version_file: 'composer.json'
version_file_path: '.version'
prerelease: false
secrets:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e338f2f..1f2ba75 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,5 +13,5 @@ jobs:
test:
uses: saucebase-dev/saucebase/.github/workflows/test-module.yml@main
with:
- module: Demo
- dependencies: saucebase/auth saucebase/settings saucebase/billing saucebase/announcements saucebase/blog saucebase/roadmap saucebase/themes
+ module: demo
+ dependencies: saucebase/auth saucebase/billing saucebase/blog saucebase/roadmap
diff --git a/LICENSE b/LICENSE
index f9a509b..cf78a69 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2025 Saucebase
+Copyright (c) 2026 Saucebase
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Taskfile.yml b/Taskfile.yml
index 692eeee..a9e5ca8 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -9,11 +9,17 @@ tasks:
test:e2e:
desc: Run E2E tests for Demo module
- cmd: npx playwright test --project="@Demo*" {{.CLI_ARGS}}
+ cmd: npx playwright test --project="@demo*" {{.CLI_ARGS}}
interactive: true
+ # ── Database ──────────────────────────────────────────────────
+
+ db:seed:
+ desc: Seed the Demo module database
+ cmd: php artisan modules:seed --module=demo
+
# ── Code Generation ────────────────────────────────────────────
types:generate:
desc: Generate TypeScript types from PHP DTOs and enums
- cmd: php artisan module:generate-types Demo
+ cmd: php artisan module:generate-types demo
diff --git a/app/Http/Controllers/DemoController.php b/app/Http/Controllers/DemoController.php
deleted file mode 100644
index 3f10d73..0000000
--- a/app/Http/Controllers/DemoController.php
+++ /dev/null
@@ -1,35 +0,0 @@
-isEnabled('Billing')) {
- $data['products'] = Product::displayable()->get();
- }
-
- if (app('modules')->isEnabled('Blog')) {
- $posts = Post::published()
- ->with(['category', 'author'])
- ->orderByDesc('published_at')
- ->limit(3)
- ->get();
-
- $data['latestPosts'] = $posts->map(fn (Post $post) => PostData::fromPost($post));
- }
-
- return Inertia::render('Demo::Index', $data)->withSSR();
- }
-}
diff --git a/app/Providers/DemoServiceProvider.php b/app/Providers/DemoServiceProvider.php
deleted file mode 100644
index 34351ca..0000000
--- a/app/Providers/DemoServiceProvider.php
+++ /dev/null
@@ -1,16 +0,0 @@
-group(module_path('Demo', '/routes/web.php'));
- Route::middleware('api')
- ->group(module_path('Demo', '/routes/api.php'));
- }
-}
diff --git a/composer.json b/composer.json
index 6fd111f..ff44ac2 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,9 @@
{
"name": "saucebase/demo",
- "description": "",
+ "description": "Demo module",
"type": "saucebase-module",
+ "license": "proprietary",
+ "version": "0.5.0",
"authors": [
{
"name": "Saucebase",
@@ -10,13 +12,15 @@
],
"extra": {
"laravel": {
- "providers": [],
+ "providers": [
+ "Modules\\Demo\\Providers\\DemoServiceProvider"
+ ],
"aliases": {}
}
},
"autoload": {
"psr-4": {
- "Modules\\Demo\\": "app/",
+ "Modules\\Demo\\": "src/",
"Modules\\Demo\\Database\\Factories\\": "database/factories/",
"Modules\\Demo\\Database\\Seeders\\": "database/seeders/"
}
@@ -25,5 +29,6 @@
"psr-4": {
"Modules\\Demo\\Tests\\": "tests/"
}
- }
+ },
+ "minimum-stability": "stable"
}
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
new file mode 100644
index 0000000..e8265a6
--- /dev/null
+++ b/database/seeders/DatabaseSeeder.php
@@ -0,0 +1,13 @@
+call(DemoDatabaseSeeder::class);
+ }
+}
diff --git a/database/seeders/DemoDatabaseSeeder.php b/database/seeders/DemoDatabaseSeeder.php
new file mode 100644
index 0000000..1731f91
--- /dev/null
+++ b/database/seeders/DemoDatabaseSeeder.php
@@ -0,0 +1,193 @@
+call([
+ BillingDatabaseSeeder::class,
+ RoadmapDatabaseSeeder::class,
+ ]);
+
+ $proMonthly = Price::where('provider_price_id', 'price_1SyadREx2sHJcHgwCt0ReZEJ')->firstOrFail();
+ $proYearly = Price::where('provider_price_id', 'price_1SyaCQEx2sHJcHgwEC9VmwSZ')->firstOrFail();
+ $teamMonthly = Price::where('provider_price_id', 'price_1SyaL0Ex2sHJcHgwWaaTGLgo')->firstOrFail();
+ $teamYearly = Price::where('provider_price_id', 'price_1SyaLWEx2sHJcHgw3fQdYV0J')->firstOrFail();
+
+ $users = User::factory()->count(30)->create();
+ foreach ($users as $user) {
+ $user->assignRole(Role::USER->value);
+ }
+
+ $customers = collect();
+ $paymentMethods = collect();
+ foreach ($users->take(25) as $user) {
+ $customer = Customer::factory()->withAddress()->create(['user_id' => $user->id]);
+ $paymentMethod = PaymentMethod::factory()->default()->visa()->create(['customer_id' => $customer->id]);
+ $customers->push($customer);
+ $paymentMethods->push($paymentMethod);
+ }
+
+ // 12 month-start dates, oldest first, for backdating
+ $months = collect(range(11, 0))->map(fn ($i) => now()->subMonths($i)->startOfMonth());
+
+ // [customerIndex, price, state]
+ $assignments = [
+ [0, $proMonthly, 'active'],
+ [1, $proMonthly, 'active'],
+ [2, $proMonthly, 'active'],
+ [3, $proMonthly, 'active'],
+ [4, $proMonthly, 'active'],
+ [5, $proMonthly, 'active'],
+ [6, $proMonthly, 'active'],
+ [7, $proMonthly, 'active'],
+ [8, $proMonthly, 'active'],
+ [9, $proMonthly, 'active'],
+ [10, $proMonthly, 'active'],
+ [11, $proMonthly, 'active'],
+ [12, $proYearly, 'active'],
+ [13, $proYearly, 'active'],
+ [14, $proYearly, 'active'],
+ [15, $proYearly, 'active'],
+ [16, $proYearly, 'active'],
+ [17, $teamMonthly, 'active'],
+ [18, $teamMonthly, 'active'],
+ [19, $teamMonthly, 'active'],
+ [20, $teamMonthly, 'active'],
+ [21, $teamYearly, 'active'],
+ [22, $teamYearly, 'active'],
+ [23, $proMonthly, 'onTrial'],
+ [24, $proMonthly, 'onTrial'],
+ // Cancelled prior subscriptions reusing the same customers
+ [0, $proMonthly, 'cancelled'],
+ [1, $teamMonthly, 'cancelled'],
+ [2, $proYearly, 'cancelled'],
+ // Past due
+ [3, $teamMonthly, 'pastDue'],
+ [4, $proMonthly, 'pastDue'],
+ ];
+
+ $activeSubscriptions = collect();
+ $monthIndex = 0;
+
+ foreach ($assignments as [$customerIndex, $price, $state]) {
+ $customer = $customers->get($customerIndex);
+ $paymentMethod = $paymentMethods->get($customerIndex);
+ $createdAt = $months[$monthIndex % 12];
+ $monthIndex++;
+
+ $factory = Subscription::factory()->state([
+ 'customer_id' => $customer->id,
+ 'price_id' => $price->id,
+ 'payment_method_id' => $paymentMethod->id,
+ ]);
+
+ $factory = match ($state) {
+ 'cancelled' => $factory->cancelled(),
+ 'pastDue' => $factory->pastDue(),
+ 'onTrial' => $factory->onTrial(),
+ default => $factory,
+ };
+
+ $subscription = $factory->create(['created_at' => $createdAt]);
+
+ if (in_array($state, ['active', 'onTrial', 'pastDue'])) {
+ $customer->user->assignRole(Role::SUBSCRIBER->value);
+ }
+
+ if ($state === 'active') {
+ $activeSubscriptions->push($subscription);
+ }
+ }
+
+ // Payments: one per month per active subscription since its creation.
+ // This produces a naturally growing monthly revenue trend.
+ $activeSubscriptions = Subscription::whereIn('id', $activeSubscriptions->pluck('id'))->with('price')->get();
+
+ foreach ($activeSubscriptions as $subscription) {
+ $cursor = Carbon::parse($subscription->created_at)->startOfMonth();
+ $nowMonth = now()->startOfMonth();
+
+ while ($cursor <= $nowMonth) {
+ $succeeded = fake()->boolean(90);
+
+ $paymentFactory = Payment::factory()->state([
+ 'customer_id' => $subscription->customer_id,
+ 'subscription_id' => $subscription->id,
+ 'price_id' => $subscription->price_id,
+ 'amount' => $subscription->price->amount,
+ ]);
+
+ if (! $succeeded) {
+ $paymentFactory = $paymentFactory->failed();
+ }
+
+ $payment = $paymentFactory->create(['created_at' => $cursor->copy()]);
+
+ if ($succeeded) {
+ Invoice::factory()->create([
+ 'customer_id' => $subscription->customer_id,
+ 'subscription_id' => $subscription->id,
+ 'payment_id' => $payment->id,
+ 'subtotal' => $subscription->price->amount,
+ 'total' => $subscription->price->amount,
+ 'paid_at' => $cursor->copy(),
+ 'created_at' => $cursor->copy(),
+ ]);
+ }
+
+ $cursor->addMonth();
+ }
+ }
+
+ // Checkout sessions spread across 12 months for the conversion chart.
+ // ~2-3 completed, 1 abandoned, 0-1 expired per month → ~67% conversion rate.
+ foreach ($months as $month) {
+ CheckoutSession::factory()
+ ->completed()
+ ->count(fake()->numberBetween(2, 3))
+ ->create(['customer_id' => null, 'price_id' => $proMonthly->id, 'created_at' => $month]);
+
+ CheckoutSession::factory()
+ ->abandoned()
+ ->create(['customer_id' => null, 'price_id' => $proMonthly->id, 'created_at' => $month]);
+
+ if (fake()->boolean(50)) {
+ CheckoutSession::factory()
+ ->expired()
+ ->create(['customer_id' => null, 'price_id' => $proMonthly->id, 'created_at' => $month]);
+ }
+ }
+
+ // Roadmap votes: each demo user votes on 3-6 random items.
+ $roadmapItems = RoadmapItem::all();
+ foreach ($users as $user) {
+ $itemsToVote = $roadmapItems->random(fake()->numberBetween(3, 6));
+ foreach ($itemsToVote as $item) {
+ RoadmapVote::firstOrCreate(
+ ['roadmap_item_id' => $item->id, 'user_id' => $user->id],
+ ['type' => fake()->randomElement([VoteType::Up, VoteType::Down])]
+ );
+ }
+ }
+ }
+}
diff --git a/module.json b/module.json
deleted file mode 100644
index e355c4f..0000000
--- a/module.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "Demo",
- "alias": "demo",
- "description": "Demo module",
- "author": "Saucebase",
- "version": "v0.5.0",
- "keywords": [
- "demo"
- ],
- "priority": 0,
- "providers": [
- "Modules\\Demo\\Providers\\DemoServiceProvider"
- ],
- "files": []
-}
diff --git a/resources/assets/images/dashboard-admin-dark.png b/resources/assets/images/dashboard-admin-dark.png
new file mode 100644
index 0000000..5443273
Binary files /dev/null and b/resources/assets/images/dashboard-admin-dark.png differ
diff --git a/resources/assets/images/dashboard-admin-light.png b/resources/assets/images/dashboard-admin-light.png
new file mode 100644
index 0000000..dfc7d6f
Binary files /dev/null and b/resources/assets/images/dashboard-admin-light.png differ
diff --git a/resources/js/app.ts b/resources/js/app.ts
index e9ea054..3043f15 100644
--- a/resources/js/app.ts
+++ b/resources/js/app.ts
@@ -1,23 +1 @@
-// import { registerIcon } from '@/lib/navigation';
-// import IconExample from '~icons/lucide/example';
-
-import '../css/style.css';
-
-/**
- * Demo module setup
- * Called during app initialization before mounting
- */
-export function setup() {
- console.debug('Demo module loaded');
-
- // Register icons for navigation items defined in routes/navigation.php
- // registerIcon('demo', IconExample);
-}
-
-/**
- * Demo module after mount logic
- * Called after the app has been mounted
- */
-export function afterMount() {
- console.debug('Demo module after mount logic executed');
-}
+export * from './vue/app';
diff --git a/resources/js/pages/sections/FeaturesSection.vue b/resources/js/pages/sections/FeaturesSection.vue
deleted file mode 100644
index 49eb72c..0000000
--- a/resources/js/pages/sections/FeaturesSection.vue
+++ /dev/null
@@ -1,178 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- {{ $t('Built for developers') }}
-
-
- {{ $t('Everything configured. Nothing missing.') }}
-
-
- {{
- $t(
- 'A production-ready foundation with the architecture, tooling, and philosophy to support your product long-term.',
- )
- }}
-
-
-
-
-
-
-
-
-
- {{ feature.name() }}
-
-
- {{ feature.description() }}
-
-
-
-
-
-
-

-

-
-
-
-
diff --git a/resources/js/pages/sections/HeroSection.vue b/resources/js/pages/sections/HeroSection.vue
deleted file mode 100644
index a53f974..0000000
--- a/resources/js/pages/sections/HeroSection.vue
+++ /dev/null
@@ -1,161 +0,0 @@
-
-
-
-
-
-
-
-
- {{ $t('With Saucebase') }}
-
-
- {{ $t('Your foundation is ready!') }}
-
-
- {{
- $t(
- 'Build production-ready Laravel apps faster than ever.',
- )
- }}
-
-
- {{
- $t('Your recipe first. Modules for everything else')
- }}
-
-
-
-
-
-
-
- laravel new --using=saucebase/saucebase
-
-
-
-
- {{ $t('Works with the official Laravel CLI') }} -
-
- {{ $t('Laravel docs') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/resources/js/types/page-props.d.ts b/resources/js/types/page-props.d.ts
index acfca86..01e84f4 100644
--- a/resources/js/types/page-props.d.ts
+++ b/resources/js/types/page-props.d.ts
@@ -2,7 +2,7 @@
// Augmentations here are auto-picked up when the module is enabled (no core edits needed).
// See docs: architecture/module-system#typescript-page-props
-// declare module '@inertiajs/vue3' {
+// declare module '@inertiajs/core' {
// interface PageProps {
// // my_shared_prop?: MyType;
// }
diff --git a/resources/js/vue/app.ts b/resources/js/vue/app.ts
new file mode 100644
index 0000000..17a4e1c
--- /dev/null
+++ b/resources/js/vue/app.ts
@@ -0,0 +1,23 @@
+// import { registerIcon } from '@/lib/navigation';
+// import IconExample from '~icons/lucide/example';
+
+import '@modules/demo/resources/css/style.css';
+
+/**
+ * Demo module setup
+ * Called during app initialization before mounting
+ */
+export function setup() {
+ console.debug('Demo module loaded');
+
+ // Register icons for navigation items defined in routes/navigation.php
+ // registerIcon('demo', IconExample);
+}
+
+/**
+ * Demo module after mount logic
+ * Called after the app has been mounted
+ */
+export function afterMount() {
+ console.debug('Demo module after mount logic executed');
+}
diff --git a/resources/js/pages/Index.vue b/resources/js/vue/pages/Index.vue
similarity index 90%
rename from resources/js/pages/Index.vue
rename to resources/js/vue/pages/Index.vue
index a7b80cd..c7500bd 100644
--- a/resources/js/pages/Index.vue
+++ b/resources/js/vue/pages/Index.vue
@@ -7,11 +7,11 @@ import FilamentSection from './sections/FilamentSection.vue';
import { Head } from '@inertiajs/vue3';
-import ProductSection from '@modules/Billing/resources/js/components/ProductSection.vue';
-import type { Product } from '@modules/Billing/resources/js/types';
-import LatestPostsSection from '@modules/Blog/resources/js/components/LatestPostsSection.vue';
+import ProductSection from '@modules/billing/resources/js/vue/components/ProductSection.vue';
+import type { Product } from '@modules/billing/resources/js/types';
+import LatestPostsSection from '@modules/blog/resources/js/vue/components/LatestPostsSection.vue';
import HeroSection from './sections/HeroSection.vue';
-import Testimonial from './sections/Testimonial.vue';
+import ModulesSection from './sections/ModulesSection.vue';
defineProps<{
products?: Product[];
@@ -36,9 +36,9 @@ defineProps<{
+
-
+import { trans } from 'laravel-vue-i18n';
+import {
+ BookOpen,
+ Braces,
+ Component,
+ FlaskConical,
+ Languages,
+ LayoutDashboard,
+ Paintbrush,
+ Terminal,
+} from 'lucide-vue-next';
+
+const features = [
+ {
+ name: () => trans('One-command setup'),
+ description: () => trans("One command. You're running."),
+ icon: Terminal,
+ },
+ {
+ name: () => trans('Full TypeScript'),
+ description: () => trans('PHP types generate TypeScript automatically.'),
+ icon: Braces,
+ },
+ {
+ name: () => trans('Theming system'),
+ description: () => trans('15 themes and a live editor.'),
+ icon: Paintbrush,
+ },
+ {
+ name: () => trans('Filament admin'),
+ description: () => trans('Admin panel ready on day one.'),
+ icon: LayoutDashboard,
+ },
+ {
+ name: () => trans('Testing suite'),
+ description: () => trans('PHPUnit and Playwright E2E, pre-configured per module.'),
+ icon: FlaskConical,
+ },
+ {
+ name: () => trans('Multi-language'),
+ description: () => trans('From day one.'),
+ icon: Languages,
+ },
+ {
+ name: () => trans('shadcn UI'),
+ description: () => trans('Beautiful components with dark mode built in.'),
+ icon: Component,
+ },
+ {
+ name: () => trans('Complete documentation'),
+ description: () => trans('Module guides, architecture docs, and AI context files for every module.'),
+ icon: BookOpen,
+ },
+];
+
+
+
+
+
+
+
+
+
+
+ {{ $t('Built for developers') }}
+
+
+ {{ $t('Everything configured. Nothing missing.') }}
+
+
+ {{
+ $t(
+ `A production-ready foundation with the architecture,
+ tooling, and philosophy to support your product long-term.`,
+ )
+ }}
+
+
+
+
+
+
+
+
+ {{ feature.name() }}
+
+
+ {{ feature.description() }}
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/pages/sections/FilamentSection.vue b/resources/js/vue/pages/sections/FilamentSection.vue
similarity index 95%
rename from resources/js/pages/sections/FilamentSection.vue
rename to resources/js/vue/pages/sections/FilamentSection.vue
index b510be7..a29e7c4 100644
--- a/resources/js/pages/sections/FilamentSection.vue
+++ b/resources/js/vue/pages/sections/FilamentSection.vue
@@ -75,7 +75,7 @@ const features = [
diff --git a/resources/js/vue/pages/sections/HeroSection.vue b/resources/js/vue/pages/sections/HeroSection.vue
new file mode 100644
index 0000000..1831b9c
--- /dev/null
+++ b/resources/js/vue/pages/sections/HeroSection.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+ {{ $t('With Saucebase') }}
+
+
+ {{ $t('Your foundation is ready!') }}
+
+
+
+
+ {{ $t('Build production-ready Laravel apps faster than ever.') }}
+
+
+ {{ $t('Your recipe first. Modules for everything else') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ currentText }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/vue/pages/sections/ModulesSection.vue b/resources/js/vue/pages/sections/ModulesSection.vue
new file mode 100644
index 0000000..3524825
--- /dev/null
+++ b/resources/js/vue/pages/sections/ModulesSection.vue
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Vue
+
+ {{ $t('The progressive framework') }}
+
+
+
+
+
+
+
React
+
+ {{ $t('The industry standard') }}
+
+
+
+
+
+
+
+ {{ $t('Pick your side') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/pages/sections/TechStackSection.vue b/resources/js/vue/pages/sections/TechStackSection.vue
similarity index 100%
rename from resources/js/pages/sections/TechStackSection.vue
rename to resources/js/vue/pages/sections/TechStackSection.vue
diff --git a/resources/js/pages/sections/Testimonial.vue b/resources/js/vue/pages/sections/Testimonial.vue
similarity index 100%
rename from resources/js/pages/sections/Testimonial.vue
rename to resources/js/vue/pages/sections/Testimonial.vue
diff --git a/routes/api.php b/routes/api.php
deleted file mode 100644
index c3115d1..0000000
--- a/routes/api.php
+++ /dev/null
@@ -1,8 +0,0 @@
-prefix('api/v1/demo')->group(function () {
-// Route::apiResource('demo', DemoController::class, ['as' => 'api']);
-// });
diff --git a/routes/navigation.php b/routes/navigation.php
index 73cde33..d29e9de 100644
--- a/routes/navigation.php
+++ b/routes/navigation.php
@@ -14,33 +14,15 @@
*/
// Landing Page Navigation
-Navigation::add('Pricing', '/#pricing', function (Section $section) {
+Navigation::add('Stripe Integration', '/#pricing', function (Section $section) {
$section->attributes([
'group' => 'landing',
- 'slug' => 'pricing',
+ 'slug' => 'stripe-integration',
'external' => true,
'order' => 1,
]);
});
-Navigation::add('Features', '/#features', function (Section $section) {
- $section->attributes([
- 'group' => 'landing',
- 'slug' => 'features',
- 'external' => true,
- 'order' => 0,
- ]);
-});
-
-// Navigation::add('FAQ', '/#faq', function (Section $section) {
-// $section->attributes([
-// 'group' => 'landing',
-// 'slug' => 'faq',
-// 'external' => true,
-// 'order' => 1,
-// ]);
-// });
-
Navigation::add(
'Docs',
'https://saucebase-dev.github.io/docs/',
diff --git a/routes/web.php b/routes/web.php
deleted file mode 100644
index 7728046..0000000
--- a/routes/web.php
+++ /dev/null
@@ -1,6 +0,0 @@
-get();
+
+ $posts = Post::published()
+ ->with(['category', 'author'])
+ ->orderByDesc('published_at')
+ ->limit(3)
+ ->get();
+
+ $data['latestPosts'] = $posts->map(fn (Post $post) => PostData::fromPost($post));
+
+ return Inertia::render('Demo::Index', $data)->withSSR();
+ }
+}
diff --git a/src/Providers/DemoServiceProvider.php b/src/Providers/DemoServiceProvider.php
new file mode 100644
index 0000000..d15c14b
--- /dev/null
+++ b/src/Providers/DemoServiceProvider.php
@@ -0,0 +1,9 @@
+expectNotToPerformAssertions();
+
+ $this->seed(DatabaseSeeder::class);
+ }
+}