feat: Implement centralized module management system

- Add ModuleRegistry for managing built-in modules (newsletter, wishlist, affiliate, subscription, licensing)
- Add ModulesController REST API for module enable/disable
- Create Modules settings page with category grouping and toggle controls
- Integrate module checks across admin-spa and customer-spa
- Add useModules hook for both SPAs to check module status
- Hide newsletter from footer builder when module disabled
- Hide wishlist features when module disabled (product cards, account menu, wishlist page)
- Protect wishlist API endpoints with module checks
- Auto-update navigation tree when modules toggled
- Clean up obsolete documentation files
- Add comprehensive documentation:
  - MODULE_SYSTEM_IMPLEMENTATION.md
  - MODULE_INTEGRATION_SUMMARY.md
  - ADDON_MODULE_INTEGRATION.md (proposal)
  - ADDON_MODULE_DESIGN_DECISIONS.md (design doc)
  - FEATURE_ROADMAP.md
  - SHIPPING_INTEGRATION.md

Module system provides:
- Centralized enable/disable for all features
- Automatic navigation updates
- Frontend/backend integration
- Foundation for addon-module unification
This commit is contained in:
Dwindi Ramadhana
2025-12-26 19:19:49 +07:00
parent 0b2c8a56d6
commit 07020bc0dd
59 changed files with 3891 additions and 12132 deletions

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { toast } from 'sonner';
import { useModules } from '@/hooks/useModules';
interface NewsletterFormProps {
description?: string;
@@ -8,6 +9,12 @@ interface NewsletterFormProps {
export function NewsletterForm({ description }: NewsletterFormProps) {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const { isEnabled } = useModules();
// Don't render if newsletter module is disabled
if (!isEnabled('newsletter')) {
return null;
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

View File

@@ -6,6 +6,7 @@ import { Button } from './ui/button';
import { useLayout } from '@/contexts/ThemeContext';
import { useShopSettings } from '@/hooks/useAppearanceSettings';
import { useWishlist } from '@/hooks/useWishlist';
import { useModules } from '@/hooks/useModules';
interface ProductCardProps {
product: {
@@ -28,8 +29,10 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
const { isClassic, isModern, isBoutique, isLaunch } = useLayout();
const { layout, elements, addToCart, saleBadge, isLoading } = useShopSettings();
const { isEnabled: wishlistEnabled, isInWishlist, toggleWishlist } = useWishlist();
const { isEnabled: isModuleEnabled } = useModules();
const inWishlist = wishlistEnabled && isInWishlist(product.id);
const showWishlist = isModuleEnabled('wishlist') && wishlistEnabled;
const inWishlist = showWishlist && isInWishlist(product.id);
const handleWishlistClick = async (e: React.MouseEvent) => {
e.preventDefault();
@@ -142,7 +145,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
)}
{/* Wishlist Button */}
{wishlistEnabled && (
{showWishlist && (
<div className="absolute top-2 left-2 z-10">
<button
onClick={handleWishlistClick}
@@ -246,7 +249,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
)}
{/* Wishlist Button */}
{wishlistEnabled && (
{showWishlist && (
<div className="absolute top-4 right-4 z-10">
<button
onClick={handleWishlistClick}
@@ -366,7 +369,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
)}
{/* Wishlist Button */}
{wishlistEnabled && (
{showWishlist && (
<div className="absolute top-6 left-6 z-10">
<button
onClick={handleWishlistClick}
@@ -440,7 +443,7 @@ export function ProductCard({ product, onAddToCart }: ProductCardProps) {
)}
{/* Wishlist Button */}
{wishlistEnabled && (
{showWishlist && (
<div className="absolute top-3 right-3 z-10">
<button
onClick={handleWishlistClick}

View File

@@ -0,0 +1,31 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api/client';
interface ModulesResponse {
enabled: string[];
}
/**
* Hook to check if modules are enabled
* Uses public endpoint, cached for performance
*/
export function useModules() {
const { data, isLoading } = useQuery<ModulesResponse>({
queryKey: ['modules-enabled'],
queryFn: async () => {
const response = await api.get('/modules/enabled') as any;
return response.data;
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});
const isEnabled = (moduleId: string): boolean => {
return data?.enabled?.includes(moduleId) ?? false;
};
return {
enabledModules: data?.enabled ?? [],
isEnabled,
isLoading,
};
}

View File

@@ -6,6 +6,7 @@ import { useCartStore } from '@/lib/cart/store';
import { Button } from '@/components/ui/button';
import { formatPrice } from '@/lib/currency';
import { toast } from 'sonner';
import { useModules } from '@/hooks/useModules';
interface WishlistItem {
product_id: number;
@@ -26,6 +27,32 @@ export default function Wishlist() {
const { addItem } = useCartStore();
const [items, setItems] = useState<WishlistItem[]>([]);
const [loading, setLoading] = useState(true);
const { isEnabled, isLoading: modulesLoading } = useModules();
if (modulesLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
);
}
if (!isEnabled('wishlist')) {
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="max-w-md w-full bg-yellow-50 border border-yellow-200 rounded-lg p-8 text-center">
<Heart className="h-16 w-16 text-yellow-600 mx-auto mb-4" />
<h2 className="text-2xl font-bold mb-2">Wishlist Not Available</h2>
<p className="text-gray-600 mb-6">
The wishlist feature is currently disabled.
</p>
<Button onClick={() => navigate('/')} className="w-full">
Continue Shopping
</Button>
</div>
</div>
);
}
useEffect(() => {
loadWishlist();

View File

@@ -1,6 +1,7 @@
import React, { ReactNode } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { LayoutDashboard, ShoppingBag, Download, MapPin, Heart, User, LogOut } from 'lucide-react';
import { useModules } from '@/hooks/useModules';
interface AccountLayoutProps {
children: ReactNode;
@@ -9,6 +10,7 @@ interface AccountLayoutProps {
export function AccountLayout({ children }: AccountLayoutProps) {
const location = useLocation();
const user = (window as any).woonoowCustomer?.user;
const { isEnabled } = useModules();
const wishlistEnabled = (window as any).woonoowCustomer?.settings?.wishlist_enabled !== false;
const allMenuItems = [
@@ -20,9 +22,9 @@ export function AccountLayout({ children }: AccountLayoutProps) {
{ id: 'account-details', label: 'Account Details', path: '/my-account/account-details', icon: User },
];
// Filter out wishlist if disabled
// Filter out wishlist if module disabled or settings disabled
const menuItems = allMenuItems.filter(item =>
item.id !== 'wishlist' || wishlistEnabled
item.id !== 'wishlist' || (isEnabled('wishlist') && wishlistEnabled)
);
const handleLogout = () => {

View File

@@ -5,6 +5,7 @@ import { apiClient } from '@/lib/api/client';
import { useCartStore } from '@/lib/cart/store';
import { useProductSettings } from '@/hooks/useAppearanceSettings';
import { useWishlist } from '@/hooks/useWishlist';
import { useModules } from '@/hooks/useModules';
import { Button } from '@/components/ui/button';
import Container from '@/components/Layout/Container';
import { ProductCard } from '@/components/ProductCard';
@@ -25,6 +26,7 @@ export default function Product() {
const thumbnailsRef = useRef<HTMLDivElement>(null);
const { addItem } = useCartStore();
const { isEnabled: wishlistEnabled, isInWishlist, toggleWishlist, isLoggedIn } = useWishlist();
const { isEnabled: isModuleEnabled } = useModules();
// Fetch product details by slug
const { data: product, isLoading, error } = useQuery<ProductType | null>({
@@ -487,7 +489,7 @@ export default function Product() {
<ShoppingCart className="h-5 w-5" />
Add to Cart
</button>
{wishlistEnabled && (
{isModuleEnabled('wishlist') && wishlistEnabled && (
<button
onClick={() => product && toggleWishlist(product.id)}
className={`w-full h-14 flex items-center justify-center gap-2 rounded-xl font-semibold text-base border-2 transition-all ${