Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions apps/api/src/api/controllers/contact.controller.ts
Original file line number Diff line number Diff line change
@@ -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<SubmitContactResponse | SubmitContactErrorResponse>
): Promise<void> => {
if (!config.spreadsheet.contactSheetId) {
throw new Error("Contact sheet ID is not configured");
}
await storeDataInGoogleSpreadsheet(req, res, config.spreadsheet.contactSheetId, CONTACT_SHEET_HEADER_VALUES);
};
2 changes: 1 addition & 1 deletion apps/api/src/api/controllers/metrics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const zeroVolume = (key: string, keyName: "day" | "month"): any => ({
});

async function getMonthlyVolumes(): Promise<MonthlyVolume[]> {
const cacheKey = `monthly`;
const cacheKey = "monthly";
const cached = cache.get<MonthlyVolume[]>(cacheKey);
if (cached) return cached;

Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/api/middlewares/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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"]);
Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/api/routes/v1/contact.route.ts
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions apps/api/src/api/routes/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -85,6 +86,11 @@ router.use("/pendulum", pendulumRoutes);
*/
router.use("/storage", storageRoutes);

/**
* POST v1/contact
*/
router.use("/contact", contactRoutes);

/**
* POST v1/email
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class DistributeFeesHandler extends BasePhaseHandler {
logger.info(`Found existing distribute fee hash for ramp ${state.id}: ${existingHash}`);

const status = await this.checkExtrinsicStatus(existingHash).catch((_: unknown) => {
throw this.createRecoverableError(`Failed to check extrinsic status`);
throw this.createRecoverableError("Failed to check extrinsic status");
});

if (status === ExtrinsicStatus.Success) {
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/config/vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface SpreadsheetConfig {
googleCredentials: GoogleCredentials;
storageSheetId: string | undefined;
emailSheetId: string | undefined;
contactSheetId: string | undefined;
ratingSheetId: string | undefined;
}

Expand Down Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions apps/frontend/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/components/Accordion/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const AccordionTrigger: FC<AccordionTriggerProps> = ({ children, className = "",
<motion.div className="flex" layout>
<motion.button
className={cn(
"w-full px-6 py-4 text-left font-medium text-base text-gray-900 transition-all cursor-pointer duration-200 hover:text-blue-700 focus:outline-none md:text-lg",
"w-full cursor-pointer px-6 py-4 text-left font-medium text-base text-gray-900 transition-all duration-200 hover:text-blue-700 focus:outline-none md:text-lg",
className
)}
onClick={() => toggleValue(value)}
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/components/Avenia/AveniaField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const AveniaField: FC<AveniaFieldProps> = ({ id, label, index, validation
<div className="relative">
<Field className={cn("w-full p-2", errors[id] && "border border-red-800")} id={id} register={register(id)} {...rest} />
{id === ExtendedAveniaFieldOptions.BIRTHDATE && (
<CalendarDaysIcon className="absolute right-3 top-1/2 h-5 w-5 -translate-y-1/2 text-gray-600 pointer-events-none" />
<CalendarDaysIcon className="-translate-y-1/2 pointer-events-none absolute top-1/2 right-3 h-5 w-5 text-gray-600" />
)}
</div>
{errorMessage && <span className="mt-1 text-red-800 text-sm">{errorMessage}</span>}
Expand Down
39 changes: 21 additions & 18 deletions apps/frontend/src/components/CallToActionSection/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 = (
<>
<span>{buttonText}</span>
<PlayCircleIcon aria-hidden="true" className="w-5 group-hover:text-pink-600" />
</>
);

return (
<section className="overflow-hidden bg-blue-900 px-4 py-32 text-white md:px-10">
<div className="relative mx-auto flex flex-col justify-between sm:container md:flex-row">
Expand All @@ -45,15 +48,15 @@ export const CallToActionSection = ({
</div>
<div className="z-10 flex flex-col justify-center md:w-1/2 md:items-end">
<p className="mt-3 mb-4 text-center text-body-lg md:mt-0 md:text-end">{description}</p>
<a
className="btn btn-vortex-secondary mx-auto flex items-center gap-2 rounded-3xl px-6 md:mx-0"
href={buttonUrl}
rel="noopener noreferrer"
target="_blank"
>
<span>{buttonText}</span>
<PlayCircleIcon aria-hidden="true" className="w-5 group-hover:text-pink-600" />
</a>
{isExternal ? (
<a className={buttonClassName} href={buttonUrl} rel="noopener noreferrer" target="_blank">
{buttonContent}
</a>
) : (
<Link className={buttonClassName} to={buttonUrl}>
{buttonContent}
</Link>
)}
</div>
</div>
</section>
Expand Down
10 changes: 5 additions & 5 deletions apps/frontend/src/components/ComparisonSlider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,31 +62,31 @@ export const ComparisonSlider: React.FC<ComparisonSliderProps> = ({

return (
<div
className={`relative h-full overflow-hidden select-none group touch-none ${className}`}
className={`group relative h-full touch-none select-none overflow-hidden ${className}`}
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
ref={containerRef}
>
<img
alt={beforeAlt}
className="absolute top-0 left-0 w-full h-full object-contain pointer-events-none select-none"
className="pointer-events-none absolute top-0 left-0 h-full w-full select-none object-contain"
draggable={false}
src={beforeImage}
/>
<img
alt={afterAlt}
className="absolute top-0 left-0 w-full h-full object-contain pointer-events-none select-none"
className="pointer-events-none absolute top-0 left-0 h-full w-full select-none object-contain"
draggable={false}
src={afterImage}
style={{
clipPath: `inset(0 ${100 - sliderPosition}% 0 0)`
}}
/>
<div
className="absolute top-0 bottom-0 w-1 bg-white cursor-ew-resize z-20 group-hover:scale-110 transition-transform"
className="absolute top-0 bottom-0 z-20 w-1 cursor-ew-resize bg-white transition-transform group-hover:scale-110"
style={{ left: `${sliderPosition}%` }}
>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-white rounded-full flex items-center justify-center shadow-lg">
<div className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 flex h-8 w-8 items-center justify-center rounded-full bg-white shadow-lg">
<svg
className="text-gray-600"
fill="none"
Expand Down
49 changes: 49 additions & 0 deletions apps/frontend/src/components/ContactForm/ContactInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { useTranslation } from "react-i18next";
import Telegram from "../../assets/socials/telegram.svg";

export function ContactInfo() {
const { t } = useTranslation();

return (
<div className="rounded-lg bg-base-100 p-8">
<h2 className="mb-6 font-bold text-2xl text-gray-900">{t("pages.contact.info.title")}</h2>

<ul className="mb-8 space-y-3 text-gray-700">
<li className="flex items-center gap-3">
<CheckIcon />
{t("pages.contact.info.requestDemo")}
</li>
<li className="flex items-center gap-3">
<CheckIcon />
{t("pages.contact.info.onboardingHelp")}
</li>
<li className="flex items-center gap-3">
<CheckIcon />
{t("pages.contact.info.integrationHelp")}
</li>
</ul>

<div className="border-gray-200 border-t pt-6">
<p className="mb-3 text-gray-700">{t("pages.contact.info.technicalQuestions")}</p>
<a
className="group flex items-center gap-2 text-blue-600 transition-colors hover:text-blue-800"
href="https://t.me/vortex_fi"
rel="noopener noreferrer"
target="_blank"
>
{t("pages.contact.info.supportLink")}
<ChevronRightIcon className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</a>
</div>
</div>
);
}

function CheckIcon() {
return (
<svg className="h-5 w-5 flex-shrink-0 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M5 13l4 4L19 7" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
</svg>
);
}
Loading