commit e232c50e52a080e8cb0d449e068d7d5360f8248f Author: kirukib Date: Sun Dec 21 18:05:16 2025 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2f6a9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +dist +.storybook-static +.DS_Store +*.log +*.cache +.vscode +.idea diff --git a/.storybook/CustomizationPanel.tsx b/.storybook/CustomizationPanel.tsx new file mode 100644 index 0000000..0d4c020 --- /dev/null +++ b/.storybook/CustomizationPanel.tsx @@ -0,0 +1,239 @@ +import React, { useState, useEffect } from "react"; +import { SketchPicker, ColorResult } from "react-color"; +import { BrandingConfig, defaultBrandingConfig, popularFonts } from "../src/config/branding.config"; + +interface CustomizationPanelProps { + active?: boolean; +} + +export const CustomizationPanel: React.FC = ({ active }) => { + const [config, setConfig] = useState(defaultBrandingConfig); + const [showPrimaryPicker, setShowPrimaryPicker] = useState(false); + const [showSecondaryPicker, setShowSecondaryPicker] = useState(false); + const [showBackgroundPicker, setShowBackgroundPicker] = useState(false); + + useEffect(() => { + // Send initial config to preview + window.postMessage( + { + type: "CUSTOMIZATION_UPDATE", + config: config, + }, + "*" + ); + }, [config]); + + const handleColorChange = (colorType: "primary" | "secondary" | "background") => ( + color: ColorResult + ) => { + const newConfig = { + ...config, + colors: { + ...config.colors, + [colorType]: color.hex, + }, + }; + setConfig(newConfig); + window.postMessage( + { + type: "CUSTOMIZATION_UPDATE", + config: newConfig, + }, + "*" + ); + }; + + const handleFontChange = (font: string) => { + const newConfig = { + ...config, + font: { + family: font, + }, + }; + setConfig(newConfig); + window.postMessage( + { + type: "CUSTOMIZATION_UPDATE", + config: newConfig, + }, + "*" + ); + }; + + if (!active) return null; + + return ( +
+

Customize Branding

+ + {/* Font Selector */} +
+ + +
+ + {/* Primary Color Picker */} +
+ +
+
{ + setShowPrimaryPicker(!showPrimaryPicker); + setShowSecondaryPicker(false); + setShowBackgroundPicker(false); + }} + style={{ + width: "100%", + height: "40px", + backgroundColor: config.colors.primary, + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center", + color: "#fff", + fontWeight: "bold", + }} + > + {config.colors.primary} +
+ {showPrimaryPicker && ( +
+ +
+ )} +
+
+ + {/* Secondary Color Picker */} +
+ +
+
{ + setShowSecondaryPicker(!showSecondaryPicker); + setShowPrimaryPicker(false); + setShowBackgroundPicker(false); + }} + style={{ + width: "100%", + height: "40px", + backgroundColor: config.colors.secondary, + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center", + color: "#fff", + fontWeight: "bold", + }} + > + {config.colors.secondary} +
+ {showSecondaryPicker && ( +
+ +
+ )} +
+
+ + {/* Background Color Picker */} +
+ +
+
{ + setShowBackgroundPicker(!showBackgroundPicker); + setShowPrimaryPicker(false); + setShowSecondaryPicker(false); + }} + style={{ + width: "100%", + height: "40px", + backgroundColor: config.colors.background, + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center", + color: config.colors.background === "#F5F5F5" ? "#333" : "#fff", + fontWeight: "bold", + }} + > + {config.colors.background} +
+ {showBackgroundPicker && ( +
+ +
+ )} +
+
+
+ ); +}; diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..67b906d --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,19 @@ +import type { StorybookConfig } from "@storybook/react-webpack5"; + +const config: StorybookConfig = { + stories: ["../stories/**/*.stories.@(js|jsx|ts|tsx|mdx)"], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + ], + framework: { + name: "@storybook/react-webpack5", + options: {}, + }, + docs: { + autodocs: "tag", + }, +}; + +export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..dab5491 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,27 @@ +import type { Preview } from "@storybook/react"; +import { CustomizationDecorator } from "../stories/CustomizationDecorator"; +import React from "react"; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + layout: "fullscreen", + }, + decorators: [ + (Story) => ( + +
+ +
+
+ ), + ], +}; + +export default preview; diff --git a/package.json b/package.json new file mode 100644 index 0000000..2fedaf3 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "fortune-sys-emails", + "version": "1.0.0", + "description": "React Email templates for iGaming system with live customization", + "private": true, + "scripts": { + "dev": "storybook dev -p 6006", + "build": "storybook build", + "preview": "storybook build && npx serve storybook-static" + }, + "dependencies": { + "@react-email/components": "^0.0.20", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-color": "^2.19.3" + }, + "devDependencies": { + "@storybook/addon-essentials": "^7.6.17", + "@storybook/addon-interactions": "^7.6.17", + "@storybook/addon-links": "^7.6.17", + "@storybook/blocks": "^7.6.17", + "@storybook/react": "^7.6.17", + "@storybook/react-webpack5": "^7.6.17", + "@storybook/test": "^7.6.17", + "@types/node": "^20.11.5", + "@types/react": "^18.2.48", + "@types/react-color": "^3.0.12", + "@types/react-dom": "^18.2.18", + "storybook": "^7.6.17", + "typescript": "^5.3.3" + } +} diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000..a927ca1 --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,32 @@ +import { Link } from "@react-email/components"; +import { useBrandingConfig } from "../hooks/useBrandingConfig"; + +interface ButtonProps { + href: string; + children: React.ReactNode; + variant?: "primary" | "secondary"; +} + +export const Button = ({ href, children, variant = "primary" }: ButtonProps) => { + const config = useBrandingConfig(); + const backgroundColor = variant === "primary" ? config.colors.primary : config.colors.secondary; + + return ( + + {children} + + ); +}; diff --git a/src/components/EmailLayout.tsx b/src/components/EmailLayout.tsx new file mode 100644 index 0000000..4cf6431 --- /dev/null +++ b/src/components/EmailLayout.tsx @@ -0,0 +1,98 @@ +import { + Html, + Head, + Body, + Container, + Section, + Text, + Heading, +} from "@react-email/components"; +import { useBrandingConfig } from "../hooks/useBrandingConfig"; +import { Logo } from "./Logo"; + +interface EmailLayoutProps { + children: React.ReactNode; + title?: string; +} + +export const EmailLayout = ({ children, title }: EmailLayoutProps) => { + const config = useBrandingConfig(); + + return ( + + + + + {/* Header */} +
+ +
+ + {/* Main Content */} +
+ {title && ( + + {title} + + )} +
+ {children} +
+
+ + {/* Footer */} +
+ + © {new Date().getFullYear()} {config.companyName} + + {config.contact?.email && ( + + Contact: {config.contact.email} + + )} + {config.contact?.address && ( + + {config.contact.address} + + )} +
+
+ + + ); +}; diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx new file mode 100644 index 0000000..c191e07 --- /dev/null +++ b/src/components/Logo.tsx @@ -0,0 +1,25 @@ +import { Img } from "@react-email/components"; +import { useBrandingConfig } from "../hooks/useBrandingConfig"; + +interface LogoProps { + width?: number; + height?: number; + alt?: string; +} + +export const Logo = ({ width = 200, height = 60, alt }: LogoProps) => { + const config = useBrandingConfig(); + + return ( + {alt + ); +}; diff --git a/src/config/branding.config.ts b/src/config/branding.config.ts new file mode 100644 index 0000000..e292d2a --- /dev/null +++ b/src/config/branding.config.ts @@ -0,0 +1,47 @@ +export interface BrandingConfig { + companyName: string; + logoUrl: string; + colors: { + primary: string; + secondary: string; + background: string; + text: string; + }; + font: { + family: string; + }; + contact?: { + email?: string; + address?: string; + }; +} + +export const defaultBrandingConfig: BrandingConfig = { + companyName: "Fortune Gaming", + logoUrl: "https://via.placeholder.com/200x60/0066CC/FFFFFF?text=Logo", + colors: { + primary: "#0066CC", + secondary: "#00CC66", + background: "#F5F5F5", + text: "#333333", + }, + font: { + family: "Arial, sans-serif", + }, + contact: { + email: "support@fortunegaming.com", + }, +}; + +export const popularFonts = [ + "Arial, sans-serif", + "Helvetica, sans-serif", + "Times New Roman, serif", + "Georgia, serif", + "Verdana, sans-serif", + "Roboto, sans-serif", + "Open Sans, sans-serif", + "Lato, sans-serif", + "Montserrat, sans-serif", + "Poppins, sans-serif", +]; diff --git a/src/emails/promotional/DepositBonusEmail.tsx b/src/emails/promotional/DepositBonusEmail.tsx new file mode 100644 index 0000000..385324d --- /dev/null +++ b/src/emails/promotional/DepositBonusEmail.tsx @@ -0,0 +1,179 @@ +import { Section, Text, Heading, Hr } from "@react-email/components"; +import { EmailLayout } from "../../components/EmailLayout"; +import { Button } from "../../components/Button"; +import { useBrandingConfig } from "../../hooks/useBrandingConfig"; +import { formatCurrency, formatPercentage } from "../../utils/emailHelpers"; + +interface DepositBonusEmailProps { + playerName?: string; + bonusPercentage?: number; + maxBonus?: number; + minimumDeposit?: number; + bonusCode?: string; + depositLink?: string; + expirationDate?: Date | string; +} + +export const DepositBonusEmail = ({ + playerName = "Valued Player", + bonusPercentage = 100, + maxBonus = 500, + minimumDeposit = 20, + bonusCode = "BONUS100", + depositLink = "https://example.com/deposit", + expirationDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), +}: DepositBonusEmailProps) => { + const config = useBrandingConfig(); + + return ( + +
+ + Hello {playerName}, + + + + We have an exclusive deposit bonus offer waiting just for you! Boost your account + with this limited-time promotion. + + +
+ + {formatPercentage(bonusPercentage)} BONUS + + + Up to {formatCurrency(maxBonus)} + + + Minimum deposit: {formatCurrency(minimumDeposit)} + +
+ +
+ +
+ + 🎟️ Bonus Code + + +
+ + {bonusCode} + +
+
+ +
+ + ✅ How to Claim + + + 1. Make a deposit of at least {formatCurrency(minimumDeposit)} + + + 2. Enter bonus code: {bonusCode} + + + 3. Receive your {formatPercentage(bonusPercentage)} bonus instantly (up to{" "} + {formatCurrency(maxBonus)}) + +
+ +
+ +
+ +
+ + ⏰ Limited Time Offer + + + This bonus expires on {new Date(expirationDate).toLocaleDateString()}. Terms and + conditions apply. Please gamble responsibly. + +
+ + + Don't miss out on this amazing opportunity to boost your account! + + + + Best regards, +
+ The {config.companyName} Team +
+
+
+ ); +}; diff --git a/src/emails/promotional/RaffleEmail.tsx b/src/emails/promotional/RaffleEmail.tsx new file mode 100644 index 0000000..8b08146 --- /dev/null +++ b/src/emails/promotional/RaffleEmail.tsx @@ -0,0 +1,104 @@ +import { Section, Text, Heading, Hr } from "@react-email/components"; +import { EmailLayout } from "../../components/EmailLayout"; +import { Button } from "../../components/Button"; +import { useBrandingConfig } from "../../hooks/useBrandingConfig"; +import { formatCurrency, formatDate } from "../../utils/emailHelpers"; + +interface RaffleEmailProps { + raffleName?: string; + prizeAmount?: number; + entryDeadline?: Date | string; + drawDate?: Date | string; + entryLink?: string; + participantName?: string; +} + +export const RaffleEmail = ({ + raffleName = "Mega Jackpot Raffle", + prizeAmount = 10000, + entryDeadline = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + drawDate = new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), + entryLink = "https://example.com/enter-raffle", + participantName = "Valued Member", +}: RaffleEmailProps) => { + const config = useBrandingConfig(); + + return ( + +
+ + Dear {participantName}, + + + + We're excited to announce our latest raffle event! This is your chance to win big. + + +
+ + Grand Prize: {formatCurrency(prizeAmount)} + +
+ +
+ +
+ + 📋 Raffle Details + + + + Entry Deadline: {formatDate(entryDeadline)} + + + Draw Date: {formatDate(drawDate)} + + + Prize Pool: {formatCurrency(prizeAmount)} + +
+ +
+ +
+ + + Don't miss this incredible opportunity! Enter now for your chance to win the grand prize. + + + + Good luck! + + + + Best regards, +
+ The {config.companyName} Team +
+
+
+ ); +}; diff --git a/src/emails/promotional/ReferralBonusEmail.tsx b/src/emails/promotional/ReferralBonusEmail.tsx new file mode 100644 index 0000000..d817f19 --- /dev/null +++ b/src/emails/promotional/ReferralBonusEmail.tsx @@ -0,0 +1,160 @@ +import { Section, Text, Heading, Hr } from "@react-email/components"; +import { EmailLayout } from "../../components/EmailLayout"; +import { Button } from "../../components/Button"; +import { useBrandingConfig } from "../../hooks/useBrandingConfig"; +import { formatCurrency } from "../../utils/emailHelpers"; + +interface ReferralBonusEmailProps { + referrerName?: string; + referralBonus?: number; + referredBonus?: number; + referralCode?: string; + referralLink?: string; + expirationDate?: Date | string; +} + +export const ReferralBonusEmail = ({ + referrerName = "Valued Member", + referralBonus = 50, + referredBonus = 25, + referralCode = "REF123456", + referralLink = "https://example.com/refer", + expirationDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), +}: ReferralBonusEmailProps) => { + const config = useBrandingConfig(); + + return ( + +
+ + Hi {referrerName}, + + + + You've been selected to participate in our exclusive referral program! Refer your + friends and earn amazing bonuses. + + +
+ + Earn {formatCurrency(referralBonus)} for each referral! + + + Your friends get {formatCurrency(referredBonus)} too! + +
+ +
+ +
+ + 🔑 Your Referral Code + + +
+ + {referralCode} + +
+
+ +
+ + 📋 How It Works + + + 1. Share your referral link or code with friends + + + 2. They sign up using your code + + + 3. You both receive bonuses when they make their first deposit! + +
+ +
+ +
+ + + This offer expires on {new Date(expirationDate).toLocaleDateString()}. Terms and + conditions apply. + + + + Start referring today and watch your bonuses grow! + + + + Best regards, +
+ The {config.companyName} Team +
+
+
+ ); +}; diff --git a/src/emails/reports/MonthlyReportEmail.tsx b/src/emails/reports/MonthlyReportEmail.tsx new file mode 100644 index 0000000..dbbe5d8 --- /dev/null +++ b/src/emails/reports/MonthlyReportEmail.tsx @@ -0,0 +1,393 @@ +import { Section, Text, Heading, Hr, Row, Column } from "@react-email/components"; +import { EmailLayout } from "../../components/EmailLayout"; +import { useBrandingConfig } from "../../hooks/useBrandingConfig"; +import { formatCurrency, formatDate, formatPercentage } from "../../utils/emailHelpers"; + +interface MonthlyReportEmailProps { + reportMonth?: Date | string; + totalDeposits?: number; + totalWithdrawals?: number; + activeUsers?: number; + newUsers?: number; + totalRevenue?: number; + averageDeposit?: number; + retentionRate?: number; + topGames?: Array<{ name: string; players: number; revenue: number }>; + growthStats?: { + depositGrowth: number; + userGrowth: number; + revenueGrowth: number; + }; +} + +export const MonthlyReportEmail = ({ + reportMonth = new Date(), + totalDeposits = 520000, + totalWithdrawals = 340000, + activeUsers = 4850, + newUsers = 720, + totalRevenue = 180000, + averageDeposit = 125, + retentionRate = 68.5, + topGames = [ + { name: "Slot Adventure", players: 1850, revenue: 65000 }, + { name: "Blackjack Pro", players: 1320, revenue: 48000 }, + { name: "Roulette Master", players: 1150, revenue: 42000 }, + { name: "Poker Championship", players: 980, revenue: 25000 }, + ], + growthStats = { + depositGrowth: 12.5, + userGrowth: 8.3, + revenueGrowth: 15.2, + }, +}: MonthlyReportEmailProps) => { + const config = useBrandingConfig(); + const netRevenue = totalDeposits - totalWithdrawals; + const monthDate = typeof reportMonth === "string" ? new Date(reportMonth) : reportMonth; + const monthName = monthDate.toLocaleDateString("en-US", { month: "long", year: "numeric" }); + + return ( + +
+ + Comprehensive Monthly Summary Report + + + {monthName} + + + {/* Executive Summary */} +
+ + 📈 Executive Summary + + + + + + TOTAL DEPOSITS + + + {formatCurrency(totalDeposits)} + + + ↑ {formatPercentage(growthStats.depositGrowth)} vs last month + + + + + NET REVENUE + + + {formatCurrency(netRevenue)} + + + ↑ {formatPercentage(growthStats.revenueGrowth)} vs last month + + + +
+ +
+ + {/* Financial Overview */} +
+ + 💰 Financial Overview + + + + +
+ + Total Revenue + + + {formatCurrency(totalRevenue)} + +
+
+ +
+ + Total Withdrawals + + + {formatCurrency(totalWithdrawals)} + +
+
+ +
+ + Avg. Deposit + + + {formatCurrency(averageDeposit)} + +
+
+
+
+ + {/* User Analytics */} +
+ + 👥 User Analytics + + + + +
+ + Active Users + + + {activeUsers.toLocaleString()} + + + ↑ {formatPercentage(growthStats.userGrowth)} growth + +
+
+ +
+ + New Users + + + {newUsers.toLocaleString()} + +
+
+ +
+ + Retention Rate + + + {formatPercentage(retentionRate)} + +
+
+
+
+ + {/* Top Performing Games */} +
+ + 🎮 Top Performing Games + + + {topGames.map((game, index) => ( +
+ + + + #{index + 1} {game.name} + + + + + Players + + + {game.players.toLocaleString()} + + + + + Revenue + + + {formatCurrency(game.revenue)} + + + +
+ ))} +
+ +
+ + + This is an automated monthly report. For detailed analytics and insights, please log + into the admin dashboard. + + + + Best regards, +
+ The {config.companyName} Analytics Team +
+
+
+ ); +}; diff --git a/src/emails/reports/WeeklyReportEmail.tsx b/src/emails/reports/WeeklyReportEmail.tsx new file mode 100644 index 0000000..1d6860e --- /dev/null +++ b/src/emails/reports/WeeklyReportEmail.tsx @@ -0,0 +1,322 @@ +import { Section, Text, Heading, Hr, Row, Column } from "@react-email/components"; +import { EmailLayout } from "../../components/EmailLayout"; +import { useBrandingConfig } from "../../hooks/useBrandingConfig"; +import { formatCurrency, formatDate } from "../../utils/emailHelpers"; + +interface WeeklyReportEmailProps { + reportPeriod?: { start: Date | string; end: Date | string }; + totalDeposits?: number; + totalWithdrawals?: number; + activeUsers?: number; + newUsers?: number; + totalRevenue?: number; + topGames?: Array<{ name: string; players: number }>; +} + +export const WeeklyReportEmail = ({ + reportPeriod = { + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + end: new Date(), + }, + totalDeposits = 125000, + totalWithdrawals = 85000, + activeUsers = 1250, + newUsers = 180, + totalRevenue = 40000, + topGames = [ + { name: "Slot Adventure", players: 450 }, + { name: "Blackjack Pro", players: 320 }, + { name: "Roulette Master", players: 280 }, + ], +}: WeeklyReportEmailProps) => { + const config = useBrandingConfig(); + const netRevenue = totalDeposits - totalWithdrawals; + + return ( + +
+ + Weekly Summary Report + + + Period: {formatDate(reportPeriod.start)} - {formatDate(reportPeriod.end)} + + + {/* Key Metrics */} +
+ + 📈 Key Metrics + + + + +
+ + Total Deposits + + + {formatCurrency(totalDeposits)} + +
+
+ +
+ + Total Withdrawals + + + {formatCurrency(totalWithdrawals)} + +
+
+
+ + + +
+ + Net Revenue + + + {formatCurrency(netRevenue)} + +
+
+ +
+ + Total Revenue + + + {formatCurrency(totalRevenue)} + +
+
+
+
+ +
+ + {/* User Statistics */} +
+ + 👥 User Statistics + + + + + + Active Users + + + {activeUsers.toLocaleString()} + + + + + New Users + + + {newUsers.toLocaleString()} + + + +
+ + {/* Top Games */} +
+ + 🎮 Top Games This Week + + + {topGames.map((game, index) => ( +
+ + + + {index + 1}. {game.name} + + + + + {game.players.toLocaleString()} players + + + +
+ ))} +
+ + + This is an automated weekly report. For detailed analytics, please log into the admin + dashboard. + +
+
+ ); +}; diff --git a/src/hooks/useBrandingConfig.ts b/src/hooks/useBrandingConfig.ts new file mode 100644 index 0000000..5457972 --- /dev/null +++ b/src/hooks/useBrandingConfig.ts @@ -0,0 +1,17 @@ +import { useContext, createContext, ReactNode } from "react"; +import { BrandingConfig, defaultBrandingConfig } from "../config/branding.config"; + +interface BrandingContextValue { + config: BrandingConfig; + updateConfig: (updates: Partial) => void; +} + +export const BrandingContext = createContext({ + config: defaultBrandingConfig, + updateConfig: () => {}, +}); + +export const useBrandingConfig = (): BrandingConfig => { + const { config } = useContext(BrandingContext); + return config; +}; diff --git a/src/utils/emailHelpers.ts b/src/utils/emailHelpers.ts new file mode 100644 index 0000000..6b6beb5 --- /dev/null +++ b/src/utils/emailHelpers.ts @@ -0,0 +1,19 @@ +export const formatCurrency = (amount: number, currency: string = "USD"): string => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: currency, + }).format(amount); +}; + +export const formatDate = (date: Date | string): string => { + const dateObj = typeof date === "string" ? new Date(date) : date; + return new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }).format(dateObj); +}; + +export const formatPercentage = (value: number): string => { + return `${value.toFixed(2)}%`; +}; diff --git a/stories/CustomizationDecorator.tsx b/stories/CustomizationDecorator.tsx new file mode 100644 index 0000000..53079ca --- /dev/null +++ b/stories/CustomizationDecorator.tsx @@ -0,0 +1,84 @@ +import React, { useState, useCallback, useEffect } from "react"; +import { BrandingContext } from "../src/hooks/useBrandingConfig"; +import { BrandingConfig, defaultBrandingConfig } from "../src/config/branding.config"; + +interface CustomizationDecoratorProps { + children: React.ReactNode; +} + +const CUSTOMIZATION_STORAGE_KEY = "email-branding-config"; + +export const CustomizationDecorator = ({ children }: CustomizationDecoratorProps) => { + const [config, setConfig] = useState(() => { + // Load from localStorage if available + if (typeof window !== "undefined") { + const saved = localStorage.getItem(CUSTOMIZATION_STORAGE_KEY); + if (saved) { + try { + return { ...defaultBrandingConfig, ...JSON.parse(saved) }; + } catch (e) { + return defaultBrandingConfig; + } + } + } + return defaultBrandingConfig; + }); + + const updateConfig = useCallback((updates: Partial) => { + setConfig((prev) => { + const newConfig = { + ...prev, + ...updates, + colors: { + ...prev.colors, + ...(updates.colors || {}), + }, + font: { + ...prev.font, + ...(updates.font || {}), + }, + }; + + // Save to localStorage + if (typeof window !== "undefined") { + localStorage.setItem(CUSTOMIZATION_STORAGE_KEY, JSON.stringify(newConfig)); + } + + return newConfig; + }); + }, []); + + // Listen for customization updates from panel + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === CUSTOMIZATION_STORAGE_KEY && e.newValue) { + try { + const newConfig = { ...defaultBrandingConfig, ...JSON.parse(e.newValue) }; + setConfig(newConfig); + } catch (e) { + // Ignore parse errors + } + } + }; + + const handleMessage = (event: MessageEvent) => { + if (event.data?.type === "CUSTOMIZATION_UPDATE") { + updateConfig(event.data.config); + } + }; + + window.addEventListener("storage", handleStorageChange); + window.addEventListener("message", handleMessage); + + return () => { + window.removeEventListener("storage", handleStorageChange); + window.removeEventListener("message", handleMessage); + }; + }, [updateConfig]); + + return ( + + {children} + + ); +}; diff --git a/stories/DepositBonusEmail.stories.tsx b/stories/DepositBonusEmail.stories.tsx new file mode 100644 index 0000000..086d702 --- /dev/null +++ b/stories/DepositBonusEmail.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { DepositBonusEmail } from "../src/emails/promotional/DepositBonusEmail"; + +const meta: Meta = { + title: "Emails/Promotional/Deposit Bonus Email", + component: DepositBonusEmail, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + playerName: "John Doe", + bonusPercentage: 100, + maxBonus: 500, + minimumDeposit: 20, + bonusCode: "BONUS100", + depositLink: "https://example.com/deposit", + expirationDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }, +}; + +export const HighValueBonus: Story = { + args: { + playerName: "VIP Player", + bonusPercentage: 200, + maxBonus: 2000, + minimumDeposit: 100, + bonusCode: "VIP200", + depositLink: "https://example.com/deposit", + expirationDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), + }, +}; diff --git a/stories/MonthlyReportEmail.stories.tsx b/stories/MonthlyReportEmail.stories.tsx new file mode 100644 index 0000000..5836886 --- /dev/null +++ b/stories/MonthlyReportEmail.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { MonthlyReportEmail } from "../src/emails/reports/MonthlyReportEmail"; + +const meta: Meta = { + title: "Emails/Reports/Monthly Report Email", + component: MonthlyReportEmail, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + reportMonth: new Date(), + totalDeposits: 520000, + totalWithdrawals: 340000, + activeUsers: 4850, + newUsers: 720, + totalRevenue: 180000, + averageDeposit: 125, + retentionRate: 68.5, + topGames: [ + { name: "Slot Adventure", players: 1850, revenue: 65000 }, + { name: "Blackjack Pro", players: 1320, revenue: 48000 }, + { name: "Roulette Master", players: 1150, revenue: 42000 }, + { name: "Poker Championship", players: 980, revenue: 25000 }, + ], + growthStats: { + depositGrowth: 12.5, + userGrowth: 8.3, + revenueGrowth: 15.2, + }, + }, +}; + +export const HighPerformance: Story = { + args: { + reportMonth: new Date(), + totalDeposits: 1200000, + totalWithdrawals: 780000, + activeUsers: 11200, + newUsers: 1850, + totalRevenue: 420000, + averageDeposit: 180, + retentionRate: 75.2, + topGames: [ + { name: "Slot Adventure", players: 4200, revenue: 185000 }, + { name: "Blackjack Pro", players: 3100, revenue: 125000 }, + { name: "Roulette Master", players: 2800, revenue: 98000 }, + { name: "Poker Championship", players: 2100, revenue: 85000 }, + { name: "Live Casino", players: 1800, revenue: 72000 }, + ], + growthStats: { + depositGrowth: 25.8, + userGrowth: 18.5, + revenueGrowth: 32.1, + }, + }, +}; diff --git a/stories/RaffleEmail.stories.tsx b/stories/RaffleEmail.stories.tsx new file mode 100644 index 0000000..f05bd70 --- /dev/null +++ b/stories/RaffleEmail.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { RaffleEmail } from "../src/emails/promotional/RaffleEmail"; + +const meta: Meta = { + title: "Emails/Promotional/Raffle Email", + component: RaffleEmail, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + raffleName: "Mega Jackpot Raffle", + prizeAmount: 10000, + entryDeadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + drawDate: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), + entryLink: "https://example.com/enter-raffle", + participantName: "John Doe", + }, +}; + +export const HighValueRaffle: Story = { + args: { + raffleName: "Ultimate Million Dollar Raffle", + prizeAmount: 1000000, + entryDeadline: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), + drawDate: new Date(Date.now() + 21 * 24 * 60 * 60 * 1000), + entryLink: "https://example.com/enter-raffle", + participantName: "Valued Member", + }, +}; diff --git a/stories/ReferralBonusEmail.stories.tsx b/stories/ReferralBonusEmail.stories.tsx new file mode 100644 index 0000000..3555da6 --- /dev/null +++ b/stories/ReferralBonusEmail.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { ReferralBonusEmail } from "../src/emails/promotional/ReferralBonusEmail"; + +const meta: Meta = { + title: "Emails/Promotional/Referral Bonus Email", + component: ReferralBonusEmail, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + referrerName: "John Doe", + referralBonus: 50, + referredBonus: 25, + referralCode: "REF123456", + referralLink: "https://example.com/refer", + expirationDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + }, +}; + +export const PremiumReferral: Story = { + args: { + referrerName: "VIP Member", + referralBonus: 100, + referredBonus: 50, + referralCode: "VIPREF789", + referralLink: "https://example.com/refer", + expirationDate: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000), + }, +}; diff --git a/stories/WeeklyReportEmail.stories.tsx b/stories/WeeklyReportEmail.stories.tsx new file mode 100644 index 0000000..3b9c62f --- /dev/null +++ b/stories/WeeklyReportEmail.stories.tsx @@ -0,0 +1,52 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { WeeklyReportEmail } from "../src/emails/reports/WeeklyReportEmail"; + +const meta: Meta = { + title: "Emails/Reports/Weekly Report Email", + component: WeeklyReportEmail, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + reportPeriod: { + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + end: new Date(), + }, + totalDeposits: 125000, + totalWithdrawals: 85000, + activeUsers: 1250, + newUsers: 180, + totalRevenue: 40000, + topGames: [ + { name: "Slot Adventure", players: 450 }, + { name: "Blackjack Pro", players: 320 }, + { name: "Roulette Master", players: 280 }, + ], + }, +}; + +export const HighActivity: Story = { + args: { + reportPeriod: { + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + end: new Date(), + }, + totalDeposits: 350000, + totalWithdrawals: 220000, + activeUsers: 3200, + newUsers: 450, + totalRevenue: 130000, + topGames: [ + { name: "Slot Adventure", players: 1250 }, + { name: "Blackjack Pro", players: 980 }, + { name: "Roulette Master", players: 720 }, + { name: "Poker Championship", players: 560 }, + ], + }, +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b147b11 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src", ".storybook", "stories"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..af3a2e7 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["package.json"] +}