Fix some issues

This commit is contained in:
2026-01-25 20:07:37 +01:00
parent e0be1b22f9
commit 6c6a9fc58c
32 changed files with 1719 additions and 226 deletions

View File

@@ -148,7 +148,6 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -2278,6 +2277,7 @@
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",
"integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.2",
@@ -2290,6 +2290,7 @@
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
@@ -2299,6 +2300,7 @@
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz",
"integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/icu-skeleton-parser": "1.8.16",
@@ -2310,6 +2312,7 @@
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz",
"integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"tslib": "^2.8.0"
@@ -2320,6 +2323,7 @@
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz",
"integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
@@ -2748,7 +2752,6 @@
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -2988,7 +2991,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -6329,7 +6331,6 @@
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-4.10.0.tgz",
"integrity": "sha512-KrMOL+sH69htCIXCaZ4JluJ35bchuCCznyPyrbN8JXSGQfwBI1SuIEMZNwvy8L8ykj29t6sa5BAAiL7fNoLZ8A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.16"
}
@@ -6419,7 +6420,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.89.0.tgz",
"integrity": "sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/query-core": "5.89.0"
},
@@ -6911,7 +6911,6 @@
"integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -6923,7 +6922,6 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -7065,7 +7063,6 @@
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0",
@@ -7404,7 +7401,6 @@
"integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/utils": "1.6.1",
"fast-glob": "^3.3.2",
@@ -7502,7 +7498,6 @@
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8045,7 +8040,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -8251,7 +8245,6 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -8616,8 +8609,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
@@ -8799,7 +8791,6 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.21.0"
},
@@ -8842,7 +8833,8 @@
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
@@ -9326,7 +9318,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -9430,7 +9421,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -10856,7 +10846,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.2"
}
@@ -10905,7 +10894,6 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -11887,8 +11875,7 @@
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause",
"peer": true
"license": "BSD-2-Clause"
},
"node_modules/leven": {
"version": "3.1.0",
@@ -13077,7 +13064,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -13244,7 +13230,6 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -13541,7 +13526,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -13614,7 +13598,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -13680,7 +13663,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -14286,7 +14268,6 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -15169,7 +15150,6 @@
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -15701,7 +15681,6 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16113,7 +16092,6 @@
"integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -16677,7 +16655,6 @@
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "1.6.1",
"@vitest/runner": "1.6.1",
@@ -17059,7 +17036,6 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",

View File

@@ -79,6 +79,9 @@ class ApiClient {
const publicEndpoints = [
'/demo/accounts',
'/demo/session/create',
'/public/contact',
'/public/feedback',
'/public/prelaunch-subscribe',
];
// Endpoints that require authentication but not a tenant ID (user-level endpoints)

View File

@@ -0,0 +1,83 @@
/**
* Public Contact API Service
* Handles public form submissions (contact, feedback, prelaunch)
* These endpoints don't require authentication
*/
import axios from 'axios';
import { getApiUrl } from '../../config/runtime';
const publicApiClient = axios.create({
baseURL: getApiUrl(),
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Types
export interface ContactFormData {
name: string;
email: string;
phone?: string;
bakery_name?: string;
type: 'general' | 'technical' | 'sales' | 'feedback';
subject: string;
message: string;
}
export interface FeedbackFormData {
name: string;
email: string;
category: 'suggestion' | 'bug' | 'feature' | 'praise' | 'complaint';
title: string;
description: string;
rating?: number;
}
export interface PrelaunchEmailData {
email: string;
}
export interface ContactFormResponse {
success: boolean;
message: string;
}
// API Functions
export const publicContactService = {
/**
* Submit a contact form
*/
submitContactForm: async (data: ContactFormData): Promise<ContactFormResponse> => {
const response = await publicApiClient.post<ContactFormResponse>(
'/v1/public/contact',
data
);
return response.data;
},
/**
* Submit a feedback form
*/
submitFeedbackForm: async (data: FeedbackFormData): Promise<ContactFormResponse> => {
const response = await publicApiClient.post<ContactFormResponse>(
'/v1/public/feedback',
data
);
return response.data;
},
/**
* Submit a prelaunch email subscription
*/
submitPrelaunchEmail: async (data: PrelaunchEmailData): Promise<ContactFormResponse> => {
const response = await publicApiClient.post<ContactFormResponse>(
'/v1/public/prelaunch-subscribe',
data
);
return response.data;
},
};
export default publicContactService;

View File

@@ -0,0 +1,185 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Mail, Rocket, CheckCircle, Loader, ArrowLeft } from 'lucide-react';
import { Button, Input, Card } from '../../ui';
import { publicContactService } from '../../../api/services/publicContact';
interface PrelaunchEmailFormProps {
onLoginClick?: () => void;
className?: string;
}
export const PrelaunchEmailForm: React.FC<PrelaunchEmailFormProps> = ({
onLoginClick,
className = '',
}) => {
const { t } = useTranslation(['auth', 'common']);
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [error, setError] = useState<string | null>(null);
const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (!email.trim()) {
setError(t('auth:prelaunch.email_required'));
return;
}
if (!validateEmail(email)) {
setError(t('auth:prelaunch.email_invalid'));
return;
}
setIsSubmitting(true);
try {
await publicContactService.submitPrelaunchEmail({ email });
setIsSubmitted(true);
} catch {
setError(t('auth:prelaunch.submit_error'));
} finally {
setIsSubmitting(false);
}
};
if (isSubmitted) {
return (
<Card className={`max-w-lg mx-auto p-8 ${className}`}>
<div className="text-center">
<div className="mx-auto w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mb-6">
<CheckCircle className="w-8 h-8 text-green-600 dark:text-green-400" />
</div>
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-3">
{t('auth:prelaunch.success_title')}
</h2>
<p className="text-[var(--text-secondary)] mb-6">
{t('auth:prelaunch.success_message')}
</p>
<div className="space-y-3">
<Button
onClick={() => window.location.href = '/'}
variant="outline"
className="w-full"
>
<ArrowLeft className="w-4 h-4 mr-2" />
{t('auth:prelaunch.back_to_home')}
</Button>
{onLoginClick && (
<p className="text-sm text-[var(--text-secondary)]">
{t('auth:register.have_account')}{' '}
<button
onClick={onLoginClick}
className="text-[var(--color-primary)] hover:underline font-medium"
>
{t('auth:register.login_link')}
</button>
</p>
)}
</div>
</div>
</Card>
);
}
return (
<Card className={`max-w-lg mx-auto p-8 ${className}`}>
<div className="text-center mb-8">
<div className="mx-auto w-16 h-16 bg-gradient-to-br from-amber-500 to-orange-500 rounded-full flex items-center justify-center mb-6 shadow-lg">
<Rocket className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-[var(--text-primary)] mb-3">
{t('auth:prelaunch.title')}
</h1>
<p className="text-lg text-[var(--text-secondary)] mb-2">
{t('auth:prelaunch.subtitle')}
</p>
<p className="text-[var(--text-tertiary)]">
{t('auth:prelaunch.description')}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<Input
type="email"
label={t('auth:register.email')}
placeholder={t('auth:register.email_placeholder')}
value={email}
onChange={(e) => setEmail(e.target.value)}
leftIcon={<Mail className="w-5 h-5" />}
error={error || undefined}
isRequired
size="lg"
/>
<Button
type="submit"
disabled={isSubmitting}
className="w-full py-4 text-lg font-semibold"
>
{isSubmitting ? (
<>
<Loader className="w-5 h-5 mr-2 animate-spin" />
{t('auth:prelaunch.submitting')}
</>
) : (
<>
<Mail className="w-5 h-5 mr-2" />
{t('auth:prelaunch.subscribe_button')}
</>
)}
</Button>
</form>
<div className="mt-8 pt-6 border-t border-[var(--border-primary)]">
<h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">
{t('auth:prelaunch.benefits_title')}
</h3>
<ul className="space-y-2 text-sm text-[var(--text-secondary)]">
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-500" />
{t('auth:prelaunch.benefit_1')}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-500" />
{t('auth:prelaunch.benefit_2')}
</li>
<li className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-500" />
{t('auth:prelaunch.benefit_3')}
</li>
</ul>
</div>
{onLoginClick && (
<div className="mt-6 text-center">
<p className="text-sm text-[var(--text-secondary)]">
{t('auth:register.have_account')}{' '}
<button
onClick={onLoginClick}
className="text-[var(--color-primary)] hover:underline font-medium"
>
{t('auth:register.login_link')}
</button>
</p>
</div>
)}
</Card>
);
};
export default PrelaunchEmailForm;

View File

@@ -29,7 +29,12 @@ const getStripeKey = (): string => {
return import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_test_51234567890123456789012345678901234567890123456789012345678901234567890123456789012345';
};
const stripePromise = loadStripe(getStripeKey());
// Force Stripe to use test environment by loading from test endpoint
const stripePromise = loadStripe(getStripeKey(), {
stripeAccount: import.meta.env.VITE_STRIPE_ACCOUNT_ID,
apiVersion: '2023-10-16',
betas: ['elements_v2']
});
interface RegistrationContainerProps {
onSuccess?: () => void;

View File

@@ -3,13 +3,15 @@ export { default as LoginForm } from './LoginForm';
export { default as RegistrationContainer } from './RegistrationContainer';
export { default as PasswordResetForm } from './PasswordResetForm';
export { default as ProfileSettings } from './ProfileSettings';
export { default as PrelaunchEmailForm } from './PrelaunchEmailForm';
// Re-export types for convenience
export type {
export type {
LoginFormProps,
RegistrationContainerProps,
RegistrationContainerProps,
PasswordResetFormProps,
ProfileSettingsProps
ProfileSettingsProps,
PrelaunchEmailFormProps
} from './types';
// Component metadata for documentation

View File

@@ -29,6 +29,11 @@ export interface ProfileSettingsProps {
initialTab?: 'profile' | 'security' | 'preferences' | 'notifications';
}
export interface PrelaunchEmailFormProps {
onLoginClick?: () => void;
className?: string;
}
// Additional types for internal use
export type RegistrationStep = 'personal' | 'bakery' | 'security' | 'verification';

View File

@@ -52,6 +52,8 @@ const getStripePublishableKey = (): string => {
const stripePromise = loadStripe(getStripePublishableKey(), {
betas: ['elements_v2'],
locale: 'auto',
stripeAccount: import.meta.env.VITE_STRIPE_ACCOUNT_ID,
apiVersion: '2023-10-16'
});
/**

View File

@@ -11,6 +11,7 @@ import {
SUBSCRIPTION_TIERS
} from '../../api';
import { getRegisterUrl } from '../../utils/navigation';
import { PRELAUNCH_CONFIG } from '../../config/prelaunch';
type BillingCycle = 'monthly' | 'yearly';
type DisplayMode = 'landing' | 'settings' | 'selection';
@@ -411,12 +412,16 @@ export const SubscriptionPricingCards: React.FC<SubscriptionPricingCardsProps> =
)
: mode === 'settings'
? t('ui.change_subscription', 'Cambiar Suscripción')
: PRELAUNCH_CONFIG.enabled
? t('ui.notify_me', 'Avísame del Lanzamiento')
: t('ui.start_free_trial')}
</Button>
{/* Footer */}
<p className={`text-xs text-center mt-3 ${(isPopular || isSelected) && !isCurrentPlan ? 'text-white/80' : 'text-[var(--text-secondary)]'}`}>
{showPilotBanner
{PRELAUNCH_CONFIG.enabled
? t('ui.prelaunch_footer', 'Lanzamiento oficial próximamente')
: showPilotBanner
? t('ui.free_trial_footer', { months: pilotTrialMonths })
: t('ui.free_trial_footer', { months: 0 })
}

View File

@@ -0,0 +1,17 @@
/**
* Pre-launch Mode Configuration
* Uses build-time environment variables
*
* When VITE_PRELAUNCH_MODE=true:
* - Registration page shows email capture form instead of Stripe flow
* - Pricing cards link to the same page but show interest form
*
* When VITE_PRELAUNCH_MODE=false (or not set):
* - Normal registration flow with Stripe payments
*/
export const PRELAUNCH_CONFIG = {
enabled: import.meta.env.VITE_PRELAUNCH_MODE === 'false',
};
export default PRELAUNCH_CONFIG;

View File

@@ -116,6 +116,23 @@
"secure_payment": "Your payment information is protected with end-to-end encryption",
"payment_info_secure": "Your payment information is secure"
},
"prelaunch": {
"title": "Coming Soon",
"subtitle": "We're preparing something special for your bakery",
"description": "Be the first to know when we officially launch. Leave your email and we'll notify you.",
"email_required": "Email is required",
"email_invalid": "Please enter a valid email address",
"submit_error": "An error occurred. Please try again.",
"subscribe_button": "Notify Me",
"submitting": "Submitting...",
"success_title": "You're on the list!",
"success_message": "We'll send you an email when we're ready to launch. Thanks for your interest!",
"back_to_home": "Back to Home",
"benefits_title": "By subscribing you'll receive:",
"benefit_1": "Early access to launch",
"benefit_2": "Exclusive offers for early adopters",
"benefit_3": "Product news and updates"
},
"steps": {
"info": "Information",
"subscription": "Plan",

View File

@@ -157,6 +157,8 @@
"payment_details": "Payment Details",
"payment_info_secure": "Your payment information is protected with end-to-end encryption",
"updating_payment": "Updating...",
"cancel": "Cancel"
"cancel": "Cancel",
"notify_me": "Notify Me of Launch",
"prelaunch_footer": "Official launch coming soon"
}
}

View File

@@ -127,6 +127,23 @@
"go_to_login": "Ir a inicio de sesión",
"try_again": "Intentar registro de nuevo"
},
"prelaunch": {
"title": "Próximamente",
"subtitle": "Estamos preparando algo especial para tu panadería",
"description": "Sé el primero en saber cuándo lancemos oficialmente. Déjanos tu email y te avisaremos.",
"email_required": "El correo electrónico es obligatorio",
"email_invalid": "Por favor, introduce un correo electrónico válido",
"submit_error": "Ha ocurrido un error. Por favor, inténtalo de nuevo.",
"subscribe_button": "Quiero que me avisen",
"submitting": "Enviando...",
"success_title": "¡Genial! Te hemos apuntado",
"success_message": "Te enviaremos un email cuando estemos listos para el lanzamiento. ¡Gracias por tu interés!",
"back_to_home": "Volver al inicio",
"benefits_title": "Al suscribirte recibirás:",
"benefit_1": "Acceso anticipado al lanzamiento",
"benefit_2": "Ofertas exclusivas para early adopters",
"benefit_3": "Noticias y actualizaciones del producto"
},
"steps": {
"info": "Información",
"subscription": "Plan",

View File

@@ -157,6 +157,8 @@
"payment_details": "Detalles de Pago",
"payment_info_secure": "Tu información de pago está protegida con encriptación de extremo a extremo",
"updating_payment": "Actualizando...",
"cancel": "Cancelar"
"cancel": "Cancelar",
"notify_me": "Avísame del Lanzamiento",
"prelaunch_footer": "Lanzamiento oficial próximamente"
}
}

View File

@@ -12,6 +12,7 @@ import {
AlertCircle,
HelpCircle
} from 'lucide-react';
import { publicContactService } from '../../api/services/publicContact';
interface ContactMethod {
id: string;
@@ -73,25 +74,35 @@ const ContactPage: React.FC = () => {
e.preventDefault();
setSubmitStatus('loading');
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500));
// In production, this would be an actual API call
console.log('Form submitted:', formState);
setSubmitStatus('success');
setTimeout(() => {
setSubmitStatus('idle');
setFormState({
name: '',
email: '',
phone: '',
bakeryName: '',
subject: '',
message: '',
type: 'general',
try {
await publicContactService.submitContactForm({
name: formState.name,
email: formState.email,
phone: formState.phone || undefined,
bakery_name: formState.bakeryName || undefined,
type: formState.type,
subject: formState.subject,
message: formState.message,
});
}, 3000);
setSubmitStatus('success');
setTimeout(() => {
setSubmitStatus('idle');
setFormState({
name: '',
email: '',
phone: '',
bakeryName: '',
subject: '',
message: '',
type: 'general',
});
}, 3000);
} catch (error) {
console.error('Contact form submission error:', error);
setSubmitStatus('error');
setTimeout(() => setSubmitStatus('idle'), 5000);
}
};
return (

View File

@@ -13,6 +13,7 @@ import {
AlertCircle,
Star
} from 'lucide-react';
import { publicContactService } from '../../api/services/publicContact';
interface FeedbackCategory {
id: string;
@@ -90,24 +91,33 @@ const FeedbackPage: React.FC = () => {
e.preventDefault();
setSubmitStatus('loading');
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500));
// In production, this would be an actual API call
console.log('Feedback submitted:', formState);
setSubmitStatus('success');
setTimeout(() => {
setSubmitStatus('idle');
setFormState({
name: '',
email: '',
category: 'suggestion',
title: '',
description: '',
rating: 0,
try {
await publicContactService.submitFeedbackForm({
name: formState.name,
email: formState.email,
category: formState.category,
title: formState.title,
description: formState.description,
rating: formState.rating > 0 ? formState.rating : undefined,
});
}, 3000);
setSubmitStatus('success');
setTimeout(() => {
setSubmitStatus('idle');
setFormState({
name: '',
email: '',
category: 'suggestion',
title: '',
description: '',
rating: 0,
});
}, 3000);
} catch (error) {
console.error('Feedback form submission error:', error);
setSubmitStatus('error');
setTimeout(() => setSubmitStatus('idle'), 5000);
}
};
const getCategoryColor = (color: string) => {

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { RegistrationContainer } from '../../components/domain/auth';
import { RegistrationContainer, PrelaunchEmailForm } from '../../components/domain/auth';
import { PublicLayout } from '../../components/layout';
import { PRELAUNCH_CONFIG } from '../../config/prelaunch';
const RegisterPage: React.FC = () => {
const navigate = useNavigate();
@@ -14,6 +15,27 @@ const RegisterPage: React.FC = () => {
navigate('/login');
};
// Show prelaunch email form or full registration based on build-time config
if (PRELAUNCH_CONFIG.enabled) {
return (
<PublicLayout
variant="centered"
maxWidth="lg"
headerProps={{
showThemeToggle: true,
showAuthButtons: false,
showLanguageSelector: true,
variant: "minimal"
}}
>
<PrelaunchEmailForm
onLoginClick={handleLoginClick}
className="mx-auto"
/>
</PublicLayout>
);
}
return (
<PublicLayout
variant="centered"

View File

@@ -10,6 +10,7 @@ interface ImportMetaEnv {
readonly VITE_PILOT_MODE_ENABLED?: string
readonly VITE_PILOT_COUPON_CODE?: string
readonly VITE_PILOT_TRIAL_MONTHS?: string
readonly VITE_PRELAUNCH_MODE?: string
}
interface ImportMeta {