Compare commits
6 Commits
3df4b44b8d
...
v1.0.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
| 0178e828d6 | |||
| e3f7344daf | |||
| 21a7b0ade2 | |||
| d027fbeac2 | |||
| 8a751998eb | |||
| 48c3e1d013 |
1
.env
1
.env
@@ -33,3 +33,4 @@ PROJECT_NAME=klz-cables
|
|||||||
TRAEFIK_HOST=klz.localhost
|
TRAEFIK_HOST=klz.localhost
|
||||||
DIRECTUS_HOST=cms.klz.localhost
|
DIRECTUS_HOST=cms.klz.localhost
|
||||||
GATEKEEPER_PASSWORD=klz2026
|
GATEKEEPER_PASSWORD=klz2026
|
||||||
|
COOKIE_DOMAIN=localhost
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const notificationResult = await sendEmail({
|
const notificationResult = await sendEmail({
|
||||||
|
replyTo: email,
|
||||||
subject: notificationSubject,
|
subject: notificationSubject,
|
||||||
html: notificationHtml,
|
html: notificationHtml,
|
||||||
});
|
});
|
||||||
@@ -111,13 +112,20 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to send branded emails', { error });
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.error('Failed to send branded emails', {
|
||||||
|
error: errorMsg,
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
services.errors.captureException(error, { action: 'sendContactFormAction', email });
|
services.errors.captureException(error, { action: 'sendContactFormAction', email });
|
||||||
|
|
||||||
await services.notifications.notify({
|
await services.notifications.notify({
|
||||||
title: '🚨 Contact Form Error',
|
title: '🚨 Contact Form Error',
|
||||||
message: `Failed to send emails for ${name} (${email}). Error: ${JSON.stringify(error)}`,
|
message: `Failed to send emails for ${name} (${email}). Error: ${errorMsg}`,
|
||||||
priority: 8,
|
priority: 8,
|
||||||
});
|
});
|
||||||
return { success: false, error };
|
|
||||||
|
return { success: false, error: errorMsg };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function ContactForm() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await sendContactFormAction(formData);
|
const result = await sendContactFormAction(formData);
|
||||||
if (result.success) {
|
if (result?.success) {
|
||||||
trackEvent('contact_form_submission', {
|
trackEvent('contact_form_submission', {
|
||||||
form_type: 'general',
|
form_type: 'general',
|
||||||
email,
|
email,
|
||||||
|
|||||||
@@ -49,21 +49,28 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
|
|||||||
|
|
||||||
if (status === 'success') {
|
if (status === 'success') {
|
||||||
return (
|
return (
|
||||||
<div className="bg-accent/5 border border-accent/20 text-primary-dark p-4 rounded-xl text-center flex flex-col items-center animate-fade-in !mt-0">
|
<div className="bg-accent/5 border border-accent/20 text-primary-dark p-4 rounded-xl text-center animate-fade-in !mt-0 w-full">
|
||||||
<div className="w-10 h-10 bg-accent rounded-full flex items-center justify-center mx-auto mb-3 shadow-lg shadow-accent/20">
|
<div className="flex justify-center mb-3">
|
||||||
<svg
|
<div className="w-10 h-10 bg-accent rounded-full flex items-center justify-center shadow-lg shadow-accent/20">
|
||||||
className="w-5 h-5 text-primary-dark"
|
<svg
|
||||||
fill="none"
|
className="w-5 h-5 text-primary-dark"
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
stroke="currentColor"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
>
|
||||||
</svg>
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={3}
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-base font-extrabold mb-1 tracking-tight !mt-0 text-center">
|
<h3 className="text-base font-extrabold mb-1 tracking-tight !mt-0 text-center w-full">
|
||||||
{t('successTitle')}
|
{t('successTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-text-secondary text-xs leading-tight mb-4 !mt-0 text-center">
|
<p className="text-text-secondary text-xs leading-tight mb-4 !mt-0 text-center w-full">
|
||||||
{t('successDesc', { productName })}
|
{t('successDesc', { productName })}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
@@ -80,24 +87,26 @@ export default function RequestQuoteForm({ productName }: RequestQuoteFormProps)
|
|||||||
|
|
||||||
if (status === 'error') {
|
if (status === 'error') {
|
||||||
return (
|
return (
|
||||||
<div className="bg-destructive/5 border border-destructive/20 text-destructive p-4 rounded-xl text-center flex flex-col items-center animate-fade-in !mt-0">
|
<div className="bg-destructive/5 border border-destructive/20 text-destructive p-4 rounded-xl text-center animate-fade-in !mt-0 w-full">
|
||||||
<div className="w-10 h-10 bg-destructive rounded-full flex items-center justify-center mx-auto mb-3 shadow-lg shadow-destructive/20">
|
<div className="flex justify-center mb-3">
|
||||||
<svg
|
<div className="w-10 h-10 bg-destructive rounded-full flex items-center justify-center shadow-lg shadow-destructive/20">
|
||||||
className="w-5 h-5 text-destructive-foreground"
|
<svg
|
||||||
fill="none"
|
className="w-5 h-5 text-destructive-foreground"
|
||||||
viewBox="0 0 24 24"
|
fill="none"
|
||||||
stroke="currentColor"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth="3"
|
stroke="currentColor"
|
||||||
>
|
strokeWidth="3"
|
||||||
<circle cx="12" cy="12" r="10" />
|
>
|
||||||
<line x1="15" y1="9" x2="9" y2="15" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<line x1="9" y1="9" x2="15" y2="15" />
|
<line x1="15" y1="9" x2="9" y2="15" />
|
||||||
</svg>
|
<line x1="9" y1="9" x2="15" y2="15" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-base font-extrabold mb-1 tracking-tight !mt-0 text-destructive text-center">
|
<h3 className="text-base font-extrabold mb-1 tracking-tight !mt-0 text-destructive text-center w-full">
|
||||||
{t('errorTitle') || 'Submission Failed'}
|
{t('errorTitle') || 'Submission Failed'}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-destructive/80 text-xs leading-tight mb-4 !mt-0 text-center">
|
<p className="text-destructive/80 text-xs leading-tight mb-4 !mt-0 text-center w-full">
|
||||||
{t('errorDesc') || 'Something went wrong. Please try again.'}
|
{t('errorDesc') || 'Something went wrong. Please try again.'}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
4
cookies.txt
Normal file
4
cookies.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Netscape HTTP Cookie File
|
||||||
|
# https://curl.se/docs/http-cookies.html
|
||||||
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ services:
|
|||||||
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
- "traefik.http.middlewares.${PROJECT_NAME:-klz-cables}-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
||||||
# Middlewares
|
# Middlewares
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.middlewares=${PROJECT_NAME:-klz-cables}-ratelimit,${PROJECT_NAME:-klz-cables}-forward,${AUTH_MIDDLEWARE:-compress}"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}.middlewares=${PROJECT_NAME:-klz-cables}-ratelimit,${PROJECT_NAME:-klz-cables}-forward,${AUTH_MIDDLEWARE:-compress}"
|
||||||
|
- "traefik.docker.network=infra"
|
||||||
|
|
||||||
# Gatekeeper Router (to show the login page)
|
# Gatekeeper Router (to show the login page)
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.rule=Host(`gatekeeper.${TRAEFIK_HOST}`)"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-gatekeeper.rule=Host(`gatekeeper.${TRAEFIK_HOST}`)"
|
||||||
@@ -74,6 +75,7 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz-cables}-gatekeeper.loadbalancer.server.port=3000"
|
- "traefik.http.services.${PROJECT_NAME:-klz-cables}-gatekeeper.loadbalancer.server.port=3000"
|
||||||
|
- "traefik.docker.network=infra"
|
||||||
|
|
||||||
directus:
|
directus:
|
||||||
image: directus/directus:11
|
image: directus/directus:11
|
||||||
@@ -111,6 +113,7 @@ services:
|
|||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.tls=true"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.tls=true"
|
||||||
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.middlewares=${PROJECT_NAME:-klz-cables}-forward,compress"
|
- "traefik.http.routers.${PROJECT_NAME:-klz-cables}-directus.middlewares=${PROJECT_NAME:-klz-cables}-forward,compress"
|
||||||
- "traefik.http.services.${PROJECT_NAME:-klz-cables}-directus.loadbalancer.server.port=8055"
|
- "traefik.http.services.${PROJECT_NAME:-klz-cables}-directus.loadbalancer.server.port=8055"
|
||||||
|
- "traefik.docker.network=infra"
|
||||||
|
|
||||||
directus-db:
|
directus-db:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
|
|||||||
@@ -27,16 +27,18 @@ function getTransporter() {
|
|||||||
|
|
||||||
interface SendEmailOptions {
|
interface SendEmailOptions {
|
||||||
to?: string | string[];
|
to?: string | string[];
|
||||||
|
replyTo?: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
html: string;
|
html: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendEmail({ to, subject, html }: SendEmailOptions) {
|
export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions) {
|
||||||
const recipients = to || config.mail.recipients;
|
const recipients = to || config.mail.recipients;
|
||||||
|
|
||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
from: config.mail.from,
|
from: config.mail.from,
|
||||||
to: recipients,
|
to: recipients,
|
||||||
|
replyTo,
|
||||||
subject,
|
subject,
|
||||||
html,
|
html,
|
||||||
};
|
};
|
||||||
@@ -48,7 +50,8 @@ export async function sendEmail({ to, subject, html }: SendEmailOptions) {
|
|||||||
logger.info('Email sent successfully', { messageId: info.messageId, subject, recipients });
|
logger.info('Email sent successfully', { messageId: info.messageId, subject, recipients });
|
||||||
return { success: true, messageId: info.messageId };
|
return { success: true, messageId: info.messageId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error sending email', { error, subject, recipients });
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
return { success: false, error };
|
logger.error('Error sending email', { error: errorMsg, subject, recipients });
|
||||||
|
return { success: false, error: errorMsg };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -74,15 +74,15 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run --passWithNoTests",
|
"test": "vitest run --passWithNoTests",
|
||||||
"test:og": "vitest run tests/og-image.test.ts",
|
"test:og": "vitest run tests/og-image.test.ts",
|
||||||
"directus:bootstrap": "DIRECTUS_URL=http://localhost:8055 npx tsx --env-file=.env scripts/setup-directus-branding.ts",
|
"cms:bootstrap": "DIRECTUS_URL=http://localhost:8055 npx tsx --env-file=.env scripts/setup-directus-branding.ts",
|
||||||
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts",
|
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts",
|
||||||
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts",
|
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts",
|
||||||
"directus:push:staging": "./scripts/sync-directus.sh push staging",
|
"cms:push:staging": "./scripts/sync-directus.sh push staging",
|
||||||
"directus:pull:staging": "./scripts/sync-directus.sh pull staging",
|
"cms:pull:staging": "./scripts/sync-directus.sh pull staging",
|
||||||
"directus:push:testing": "./scripts/sync-directus.sh push testing",
|
"cms:push:testing": "./scripts/sync-directus.sh push testing",
|
||||||
"directus:pull:testing": "./scripts/sync-directus.sh pull testing",
|
"cms:pull:testing": "./scripts/sync-directus.sh pull testing",
|
||||||
"directus:push:prod": "./scripts/sync-directus.sh push production",
|
"cms:push:prod": "./scripts/sync-directus.sh push production",
|
||||||
"directus:pull:prod": "./scripts/sync-directus.sh pull production",
|
"cms:pull:prod": "./scripts/sync-directus.sh pull production",
|
||||||
"pagespeed:test": "tsx ./scripts/pagespeed-sitemap.ts",
|
"pagespeed:test": "tsx ./scripts/pagespeed-sitemap.ts",
|
||||||
"pagespeed:urls": "tsx -e \"import sitemap from './app/sitemap'; sitemap().then(urls => console.log(urls.map(u => u.url).join('\\n')))\"",
|
"pagespeed:urls": "tsx -e \"import sitemap from './app/sitemap'; sitemap().then(urls => console.log(urls.map(u => u.url).join('\\n')))\"",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ case $ENV in
|
|||||||
;;
|
;;
|
||||||
production)
|
production)
|
||||||
PROJECT_NAME="klz-cables-prod"
|
PROJECT_NAME="klz-cables-prod"
|
||||||
|
# Fallback to older project name if prod-specific one isn't found later in the script
|
||||||
|
OLD_PROJECT_NAME="klz-cablescom"
|
||||||
ENV_FILE=".env.prod"
|
ENV_FILE=".env.prod"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -58,6 +60,7 @@ if [ "$ACTION" == "push" ]; then
|
|||||||
|
|
||||||
# 1. DB Dump
|
# 1. DB Dump
|
||||||
echo "📦 Dumping local database..."
|
echo "📦 Dumping local database..."
|
||||||
|
# Note: we use --no-owner --no-privileges to ensure restore works on remote with different user setup
|
||||||
docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$DB_USER" --clean --if-exists --no-owner --no-privileges "$DB_NAME" > dump.sql
|
docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$DB_USER" --clean --if-exists --no-owner --no-privileges "$DB_NAME" > dump.sql
|
||||||
|
|
||||||
# 2. Upload Dump
|
# 2. Upload Dump
|
||||||
@@ -67,10 +70,21 @@ if [ "$ACTION" == "push" ]; then
|
|||||||
# 3. Restore on Remote
|
# 3. Restore on Remote
|
||||||
echo "🔄 Restoring dump on $ENV..."
|
echo "🔄 Restoring dump on $ENV..."
|
||||||
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
|
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
|
||||||
|
if [ -z "$REMOTE_DB_CONTAINER" ] && [ -n "$OLD_PROJECT_NAME" ]; then
|
||||||
|
echo "⚠️ $PROJECT_NAME not found, trying fallback $OLD_PROJECT_NAME..."
|
||||||
|
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $OLD_PROJECT_NAME ps -q directus-db")
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
||||||
echo "❌ Remote $ENV-db container not found!"
|
echo "❌ Remote $ENV-db container not found!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Wipe remote DB clean before restore to avoid constraint errors
|
||||||
|
echo "🧹 Wiping remote database schema..."
|
||||||
|
ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER psql -U $DB_USER $DB_NAME -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'"
|
||||||
|
|
||||||
|
echo "⚡ Restoring database..."
|
||||||
ssh "$REMOTE_HOST" "docker exec -i $REMOTE_DB_CONTAINER psql -U $DB_USER $DB_NAME < $REMOTE_DIR/dump.sql"
|
ssh "$REMOTE_HOST" "docker exec -i $REMOTE_DB_CONTAINER psql -U $DB_USER $DB_NAME < $REMOTE_DIR/dump.sql"
|
||||||
|
|
||||||
# 4. Sync Uploads
|
# 4. Sync Uploads
|
||||||
@@ -83,6 +97,10 @@ if [ "$ACTION" == "push" ]; then
|
|||||||
rm dump.sql
|
rm dump.sql
|
||||||
ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql"
|
ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql"
|
||||||
|
|
||||||
|
# 5. Restart Directus to trigger migrations and refresh schema cache
|
||||||
|
echo "🔄 Restarting remote Directus to apply migrations..."
|
||||||
|
ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME restart directus"
|
||||||
|
|
||||||
echo "✨ Push to $ENV complete!"
|
echo "✨ Push to $ENV complete!"
|
||||||
|
|
||||||
elif [ "$ACTION" == "pull" ]; then
|
elif [ "$ACTION" == "pull" ]; then
|
||||||
@@ -91,6 +109,11 @@ elif [ "$ACTION" == "pull" ]; then
|
|||||||
# 1. DB Dump on Remote
|
# 1. DB Dump on Remote
|
||||||
echo "📦 Dumping remote database ($ENV)..."
|
echo "📦 Dumping remote database ($ENV)..."
|
||||||
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
|
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
|
||||||
|
if [ -z "$REMOTE_DB_CONTAINER" ] && [ -n "$OLD_PROJECT_NAME" ]; then
|
||||||
|
echo "⚠️ $PROJECT_NAME not found, trying fallback $OLD_PROJECT_NAME..."
|
||||||
|
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $OLD_PROJECT_NAME ps -q directus-db")
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
||||||
echo "❌ Remote $ENV-db container not found!"
|
echo "❌ Remote $ENV-db container not found!"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -101,8 +124,11 @@ elif [ "$ACTION" == "pull" ]; then
|
|||||||
echo "📥 Downloading dump..."
|
echo "📥 Downloading dump..."
|
||||||
scp "$REMOTE_HOST:$REMOTE_DIR/dump.sql" dump.sql
|
scp "$REMOTE_HOST:$REMOTE_DIR/dump.sql" dump.sql
|
||||||
|
|
||||||
# 3. Restore Locally
|
# Wipe local DB clean before restore to avoid constraint errors
|
||||||
echo "🔄 Restoring dump locally..."
|
echo "🧹 Wiping local database schema..."
|
||||||
|
docker exec "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
|
||||||
|
|
||||||
|
echo "⚡ Restoring database locally..."
|
||||||
docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" < dump.sql
|
docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" < dump.sql
|
||||||
|
|
||||||
# 4. Sync Uploads
|
# 4. Sync Uploads
|
||||||
|
|||||||
Reference in New Issue
Block a user