diff --git a/apps/api/src/api/controllers/contact.controller.ts b/apps/api/src/api/controllers/contact.controller.ts new file mode 100644 index 000000000..083b369dc --- /dev/null +++ b/apps/api/src/api/controllers/contact.controller.ts @@ -0,0 +1,32 @@ +import type { SubmitContactErrorResponse, SubmitContactResponse } from "@vortexfi/shared"; +import type { Request, Response } from "express"; +import { config } from "../../config"; +import { storeDataInGoogleSpreadsheet } from "./googleSpreadSheet.controller"; + +enum ContactSheetHeaders { + Timestamp = "timestamp", + FullName = "fullName", + Email = "email", + ProjectName = "projectName", + Inquiry = "inquiry" +} + +const CONTACT_SHEET_HEADER_VALUES = [ + ContactSheetHeaders.Timestamp, + ContactSheetHeaders.FullName, + ContactSheetHeaders.Email, + ContactSheetHeaders.ProjectName, + ContactSheetHeaders.Inquiry +]; + +export { CONTACT_SHEET_HEADER_VALUES }; + +export const submitContact = async ( + req: Request, + res: Response +): Promise => { + if (!config.spreadsheet.contactSheetId) { + throw new Error("Contact sheet ID is not configured"); + } + await storeDataInGoogleSpreadsheet(req, res, config.spreadsheet.contactSheetId, CONTACT_SHEET_HEADER_VALUES); +}; diff --git a/apps/api/src/api/middlewares/validators.ts b/apps/api/src/api/middlewares/validators.ts index 520530626..cecf9178e 100644 --- a/apps/api/src/api/middlewares/validators.ts +++ b/apps/api/src/api/middlewares/validators.ts @@ -20,6 +20,7 @@ import { } from "@vortexfi/shared"; import { RequestHandler } from "express"; import httpStatus from "http-status"; +import { CONTACT_SHEET_HEADER_VALUES } from "../controllers/contact.controller"; import { EMAIL_SHEET_HEADER_VALUES } from "../controllers/email.controller"; import { RATING_SHEET_HEADER_VALUES } from "../controllers/rating.controller"; import { FLOW_HEADERS } from "../controllers/storage.controller"; @@ -261,6 +262,7 @@ const validateRequestBodyValues = }; export const validateStorageInput = validateRequestBodyValuesForTransactionStore(); +export const validateContactInput = validateRequestBodyValues(CONTACT_SHEET_HEADER_VALUES); export const validateEmailInput = validateRequestBodyValues(EMAIL_SHEET_HEADER_VALUES); export const validateRatingInput = validateRequestBodyValues(RATING_SHEET_HEADER_VALUES); export const validateExecuteXCM = validateRequestBodyValues(["id", "payload"]); diff --git a/apps/api/src/api/routes/v1/contact.route.ts b/apps/api/src/api/routes/v1/contact.route.ts new file mode 100644 index 000000000..e5883b19e --- /dev/null +++ b/apps/api/src/api/routes/v1/contact.route.ts @@ -0,0 +1,9 @@ +import { Router } from "express"; +import * as contactController from "../../controllers/contact.controller"; +import { validateContactInput } from "../../middlewares/validators"; + +const router: Router = Router({ mergeParams: true }); + +router.route("/submit").post(validateContactInput, contactController.submitContact); + +export default router; diff --git a/apps/api/src/api/routes/v1/index.ts b/apps/api/src/api/routes/v1/index.ts index 021d3ce1f..bbe92e349 100644 --- a/apps/api/src/api/routes/v1/index.ts +++ b/apps/api/src/api/routes/v1/index.ts @@ -4,6 +4,7 @@ import { sendStatusWithPk as sendPendulumStatusWithPk } from "../../controllers/ import { sendStatusWithPk as sendStellarStatusWithPk } from "../../controllers/stellar.controller"; import partnerApiKeysRoutes from "./admin/partner-api-keys.route"; import brlaRoutes from "./brla.route"; +import contactRoutes from "./contact.route"; import countriesRoutes from "./countries.route"; import cryptocurrenciesRoutes from "./cryptocurrencies.route"; import emailRoutes from "./email.route"; @@ -85,6 +86,11 @@ router.use("/pendulum", pendulumRoutes); */ router.use("/storage", storageRoutes); +/** + * POST v1/contact + */ +router.use("/contact", contactRoutes); + /** * POST v1/email */ diff --git a/apps/api/src/config/vars.ts b/apps/api/src/config/vars.ts index 68b5af9d1..da8fd46aa 100644 --- a/apps/api/src/config/vars.ts +++ b/apps/api/src/config/vars.ts @@ -18,6 +18,7 @@ interface SpreadsheetConfig { googleCredentials: GoogleCredentials; storageSheetId: string | undefined; emailSheetId: string | undefined; + contactSheetId: string | undefined; ratingSheetId: string | undefined; } @@ -98,6 +99,7 @@ export const config: Config = { rateLimitNumberOfProxies: process.env.RATE_LIMIT_NUMBER_OF_PROXIES || 1, rateLimitWindowMinutes: process.env.RATE_LIMIT_WINDOW_MINUTES || 1, spreadsheet: { + contactSheetId: process.env.GOOGLE_CONTACT_SPREADSHEET_ID, emailSheetId: process.env.GOOGLE_EMAIL_SPREADSHEET_ID, googleCredentials: { email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL, diff --git a/apps/frontend/App.css b/apps/frontend/App.css index 88467f798..12a75c811 100644 --- a/apps/frontend/App.css +++ b/apps/frontend/App.css @@ -117,6 +117,11 @@ .btn-vortex-primary { @apply bg-blue-700 text-white rounded-[var(--radius-field)] border border-blue-700 cursor-pointer; + transition: scale 0.1s ease-in-out; +} + +.btn-vortex-primary:active { + scale: 0.98; } .btn-vortex-primary:hover { @@ -134,6 +139,11 @@ @apply border; @apply border-gray-300; @apply duration-200; + transition: scale 0.1s ease-in-out; +} + +.btn-vortex-accent:active { + scale: 0.98; } .btn-vortex-accent:hover { @@ -149,6 +159,11 @@ @apply border; @apply border-blue-700; @apply cursor-pointer; + transition: scale 0.1s ease-in-out; +} + +.btn-vortex-primary-inverse:active { + scale: 0.98; } .btn-vortex-primary-inverse:hover { @@ -175,6 +190,11 @@ @apply bg-pink-600; @apply border-pink-600; @apply shadow-none; + transition: scale 0.1s ease-in-out; +} + +.btn-vortex-secondary:active { + scale: 0.98; } .btn-vortex-secondary:hover { @@ -191,6 +211,11 @@ @apply border; @apply border-red-600; @apply shadow-none; + transition: scale 0.1s ease-in-out; +} + +.btn-vortex-danger:active { + scale: 0.98; } .btn-vortex-danger:hover { diff --git a/apps/frontend/src/components/CallToActionSection/index.tsx b/apps/frontend/src/components/CallToActionSection/index.tsx index 6f9472cfa..613cb7106 100644 --- a/apps/frontend/src/components/CallToActionSection/index.tsx +++ b/apps/frontend/src/components/CallToActionSection/index.tsx @@ -1,4 +1,5 @@ import { PlayCircleIcon } from "@heroicons/react/20/solid"; +import { Link } from "@tanstack/react-router"; import { motion } from "motion/react"; import { ReactNode } from "react"; import PLANET from "../../assets/planet.svg"; @@ -8,22 +9,24 @@ interface CallToActionSectionProps { description: string; buttonText: string; buttonUrl?: string; + isExternal?: boolean; } -/** - * CallToActionSection - Reusable CTA section with animated planet background - * Features: - * - Animated planet image with hover effects - * - Flexible title (string or ReactNode for custom styling) - * - Responsive layout - * - Configurable button text and URL - */ export const CallToActionSection = ({ title, description, buttonText, - buttonUrl = "https://forms.gle/dKh8ckXheRPdRa398" + buttonUrl = "", + isExternal = true }: CallToActionSectionProps) => { + const buttonClassName = "btn btn-vortex-secondary mx-auto flex items-center gap-2 rounded-3xl px-6 md:mx-0"; + const buttonContent = ( + <> + {buttonText} +