Fix some issues
This commit is contained in:
42
frontend/package-lock.json
generated
42
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
83
frontend/src/api/services/publicContact.ts
Normal file
83
frontend/src/api/services/publicContact.ts
Normal 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;
|
||||
185
frontend/src/components/domain/auth/PrelaunchEmailForm.tsx
Normal file
185
frontend/src/components/domain/auth/PrelaunchEmailForm.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
17
frontend/src/config/prelaunch.ts
Normal file
17
frontend/src/config/prelaunch.ts
Normal 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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
1
frontend/src/vite-env.d.ts
vendored
1
frontend/src/vite-env.d.ts
vendored
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user