Merge branch 'master' into nested-country-managers
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import About from "@/templates/About";
|
||||
import ComingSoon from "@/templates/ComingSoon";
|
||||
import ContactUs from "@/templates/ContactUs";
|
||||
|
||||
export default function Page() {
|
||||
return <ComingSoon page="/contact" language="ar" />;
|
||||
return <ContactUs page="/contact" language="ar" />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import About from "@/templates/About";
|
||||
import ComingSoon from "@/templates/ComingSoon";
|
||||
import ContactUs from "@/templates/ContactUs";
|
||||
|
||||
export default function Page() {
|
||||
return <ComingSoon page="/contact" language="en" />;
|
||||
return <ContactUs page="/contact" language="en" />;
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Link from "next/link";
|
||||
import {BsInstagram, BsTwitter} from "react-icons/bs";
|
||||
import {BiLogoFacebook} from "react-icons/bi";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import PricingTable from "@/components/PricingTable";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="h-screen w-full bg-white text-mti-black flex flex-col">
|
||||
<Navbar currentPage="/join" language="en" />
|
||||
|
||||
<section className="w-full relative bg-white py-20 lg:py-48 px-8 flex flex-col items-center text-center gap-4">
|
||||
<h2 className="text-3xl font-bold">Available Packages</h2>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="max-w-lg">
|
||||
Once the payment process is complete, you will receive a code via e-mail to register to the application.
|
||||
</span>
|
||||
<span className="max-w-lg">(Or have time added to your account, if already registered with the given e-mail).</span>
|
||||
<PricingTable />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer language="en" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @next/next/no-page-custom-font */
|
||||
import clsx from "clsx";
|
||||
import "./globals.css";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import type {Metadata} from "next";
|
||||
import {Inter} from "next/font/google";
|
||||
import {Almarai} from "next/font/google";
|
||||
@@ -26,9 +27,6 @@ export default function RootLayout({children}: {children: React.ReactNode}) {
|
||||
httpEquiv="Content-Security-Policy-Report-Only"
|
||||
content="default-src 'self' *.stripe.com *.encoach.com staging.encoach.com localhost"
|
||||
/>
|
||||
|
||||
<script async src="https://js.stripe.com/v3/pricing-table.js" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Almarai:wght@300;400;700;800&display=swap" rel="stylesheet" />
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Elements} from "@stripe/react-stripe-js";
|
||||
import {loadStripe} from "@stripe/stripe-js";
|
||||
|
||||
const stripePromise = loadStripe(process.env.STRIPE_KEY || "");
|
||||
|
||||
export default function PricingTable() {
|
||||
return (
|
||||
<Elements stripe={stripePromise}>
|
||||
<stripe-pricing-table id="pricing" pricing-table-id={process.env.STRIPE_PRICING_TABLE} publishable-key={process.env.STRIPE_KEY} />
|
||||
</Elements>
|
||||
);
|
||||
}
|
||||
|
||||
// If using TypeScript, add the following snippet to your file as well.
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"stripe-pricing-table": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
|
||||
}
|
||||
}
|
||||
}
|
||||
189
src/templates/ContactUs.tsx
Normal file
189
src/templates/ContactUs.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useForm, SubmitHandler, Controller } from "react-hook-form";
|
||||
import Footer from "@/components/Footer";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Title from "@/components/Title";
|
||||
import translation from "@/translation/contactus.json";
|
||||
import Image from "next/image";
|
||||
import { toast, ToastContainer } from "react-toastify";
|
||||
|
||||
type FormValues = {
|
||||
name: string;
|
||||
email: string;
|
||||
description: string;
|
||||
subject: string;
|
||||
type: "feedback" | "bug" | "help" | "";
|
||||
};
|
||||
|
||||
interface Props {
|
||||
language: "en" | "ar";
|
||||
page: string;
|
||||
}
|
||||
|
||||
const ErrorMessage = ({ message }: { message: string }) => (
|
||||
<div className="w-full">
|
||||
<span className="text-mti-red">{message}</span>
|
||||
</div>
|
||||
);
|
||||
export default function App({ language, page }: Props) {
|
||||
const selectOptions = [
|
||||
{
|
||||
label: translation.feedback[language],
|
||||
value: "feedback",
|
||||
},
|
||||
{
|
||||
label: translation.bug[language],
|
||||
value: "bug",
|
||||
},
|
||||
{
|
||||
label: translation.help[language],
|
||||
value: "help",
|
||||
},
|
||||
];
|
||||
|
||||
const { register, control, handleSubmit, formState } = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
description: "",
|
||||
subject: "",
|
||||
type: "",
|
||||
},
|
||||
});
|
||||
|
||||
const { errors, isDirty, isValid } = formState;
|
||||
const onSubmit: SubmitHandler<FormValues> = async (data) => {
|
||||
try {
|
||||
const response = await fetch(`https://platform.encoach.com/api/tickets`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reporter: {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
},
|
||||
subject: data.subject,
|
||||
type: data.type,
|
||||
reportedFrom: window.location.href,
|
||||
status: "submitted",
|
||||
date: new Date().toISOString(),
|
||||
description: data.description,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
// Pass data to the page via props
|
||||
if (data.ok) {
|
||||
toast.success(translation.ticketSuccess[language]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
toast.error(translation.ticketError[language]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToastContainer />
|
||||
<main
|
||||
className="text-mti-black flex h-screen w-full flex-col bg-white"
|
||||
dir={language === "ar" ? "rtl" : "ltr"}
|
||||
>
|
||||
<Navbar currentPage={page} language={language} />
|
||||
<section className="w-full bg-mti-purple text-white text-center p-8 md:p-16">
|
||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
||||
<Title>{translation.title[language]}</Title>
|
||||
</div>
|
||||
</section>
|
||||
<section className="w-full bg-white text-center p-8 md:p-16 flex justify-center items-center gap-32">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="form-control items-center gap-2 text-mti-black flex flex-col w-96"
|
||||
>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder={translation.name[language]}
|
||||
{...register("name", { required: true })}
|
||||
className="input input-bordered md:w-full sm:w-1/2 max-w-md"
|
||||
/>
|
||||
{errors.name && errors.name.type === "required" && (
|
||||
<ErrorMessage message={translation.fieldRequired[language]} />
|
||||
)}
|
||||
<input
|
||||
id="email"
|
||||
placeholder={translation.email[language]}
|
||||
type="text"
|
||||
{...register("email", { required: true, pattern: /^\S+@\S+$/i })}
|
||||
className="input input-bordered md:w-full sm:w-1/2 max-w-md"
|
||||
/>
|
||||
{errors.email && errors.email.type === "required" && (
|
||||
<ErrorMessage message={translation.fieldRequired[language]} />
|
||||
)}
|
||||
{errors.email && errors.email.type === "pattern" && (
|
||||
<ErrorMessage message={translation.invalidEmail[language]} />
|
||||
)}
|
||||
<input
|
||||
id="subject"
|
||||
placeholder={translation.subject[language]}
|
||||
type="text"
|
||||
{...register("subject", { required: true })}
|
||||
className="input input-bordered md:w-full sm:w-1/2 max-w-md"
|
||||
/>
|
||||
{errors.subject && errors.subject.type === "required" && (
|
||||
<ErrorMessage message={translation.fieldRequired[language]} />
|
||||
)}
|
||||
<select
|
||||
id="type"
|
||||
{...register("type", { required: true })}
|
||||
className="select select-bordered md:w-full sm:w-1/2 max-w-md"
|
||||
>
|
||||
<option value="" disabled>
|
||||
{translation.selectType[language]}
|
||||
</option>
|
||||
{selectOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.type && errors.type.type === "required" && (
|
||||
<ErrorMessage message={translation.fieldRequired[language]} />
|
||||
)}
|
||||
<textarea
|
||||
id="description"
|
||||
placeholder={translation.description[language]}
|
||||
{...register("description", { required: true })}
|
||||
className="textarea textarea-bordered md:w-full sm:w-1/2 max-w-md"
|
||||
rows={5}
|
||||
/>
|
||||
{errors.description && errors.description.type === "required" && (
|
||||
<ErrorMessage message={translation.fieldRequired[language]} />
|
||||
)}
|
||||
<input
|
||||
type="submit"
|
||||
className="btn"
|
||||
disabled={!isDirty || !isValid}
|
||||
value={translation.submit[language]}
|
||||
/>
|
||||
</form>
|
||||
<div className="flex flex-col">
|
||||
<Image
|
||||
src="/person_laptop_focus.jpg"
|
||||
alt="Contact Us"
|
||||
width={500}
|
||||
height={340}
|
||||
className="rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<Footer language={language} />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ const Element = ({date, label, isEven, language, Icon}: ElementProps) => {
|
||||
<span className={clsx("w-64 text-mti-purple-light font-semibold text-lg", isEven && "text-right")}>{date}</span>
|
||||
</div>
|
||||
|
||||
<div className={clsx("flex items-center gap-8 md:hidden", language === "ar" && "flex-row-reverse text-right")}>
|
||||
<div className={clsx("flex items-center gap-8 md:hidden")}>
|
||||
<div className="h-16 w-16 rounded-full bg-mti-purple-dark flex items-center justify-center text-white border-white border-4 drop-shadow">
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
@@ -75,9 +75,9 @@ export default function History({language}: Props) {
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"w-2 h-[89%] bg-mti-purple-light absolute md:left-1/2 md:-translate-x-1/2 top-1/2 -translate-y-1/2 rounded-full",
|
||||
language === "en" && "left-1/2 -translate-x-[148px]",
|
||||
language === "ar" && "right-1/2 translate-x-[148px]",
|
||||
"w-2 h-[89%] bg-mti-purple-light absolute top-1/2 -translate-y-1/2 rounded-full",
|
||||
language === "en" && "left-1/2 -translate-x-[148px] md:left-1/2 md:-translate-x-1/2",
|
||||
language === "ar" && "right-1/2 translate-x-[148px] md:right-1/2 md:translate-x-1/2",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function Home({language}: Props) {
|
||||
<Title>{translation.learn_ai.title[language]}</Title>
|
||||
<p className="max-w-lg text-base">{translation.learn_ai.description[language]}</p>
|
||||
</div>
|
||||
<Link href="/join">
|
||||
<Link href="/price">
|
||||
<button className="bg-mti-purple-light hover:bg-mti-purple text-white rounded-xl px-8 py-4 transition ease-in-out duration-300 shadow">
|
||||
{translation.learn_more[language]}
|
||||
</button>
|
||||
|
||||
@@ -21,6 +21,28 @@ interface Package {
|
||||
}
|
||||
|
||||
export default function Page({language}: {language: "en" | "ar"}) {
|
||||
const getDurationUnit = (duration: number, durationUnitSingular: string, durationUnitPlural: string) => {
|
||||
if(duration >= 2 && duration <= 10) {
|
||||
return durationUnitPlural;
|
||||
}
|
||||
|
||||
return durationUnitSingular;
|
||||
}
|
||||
const durationAndDurationUnitParser = (duration: number, duration_unit: DurationUnit) => {
|
||||
if(language === 'ar') {
|
||||
switch (duration_unit) {
|
||||
case "days":
|
||||
return `${duration} ${getDurationUnit(duration, translation.days.singular[language], translation.days.plural[language])}`;
|
||||
case "weeks":
|
||||
return `${duration} ${getDurationUnit(duration, translation.weeks.singular[language], translation.weeks.plural[language])}`;
|
||||
case "months":
|
||||
return `${duration} ${getDurationUnit(duration, translation.months.singular[language], translation.months.plural[language])}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `${duration} ${capitalize(duration === 1 ? duration_unit.slice(0, duration_unit.length - 1) : duration_unit)}`;
|
||||
}
|
||||
|
||||
const [payments, setPayments] = React.useState<Package[]>([]);
|
||||
const getData = async () => {
|
||||
// Fetch data from external API
|
||||
@@ -50,8 +72,7 @@ export default function Page({language}: {language: "en" | "ar"}) {
|
||||
<div className="flex flex-col items-start mb-2">
|
||||
<Image src="/logo_title.png" alt="EnCoach's Logo" width={32} height={32} />
|
||||
<span className="font-semibold text-xl">
|
||||
EnCoach - {p.duration}{" "}
|
||||
{capitalize(p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit)}
|
||||
{translation.encoach[language]} - {durationAndDurationUnitParser(p.duration, p.duration_unit)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-start w-full">
|
||||
|
||||
58
src/translation/contactus.json
Normal file
58
src/translation/contactus.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"title": {
|
||||
"en": "Submit a ticket",
|
||||
"ar": "تحدث إلينا "
|
||||
},
|
||||
"name": {
|
||||
"en": "Name",
|
||||
"ar": "الإسم"
|
||||
},
|
||||
"email": {
|
||||
"en": "Email",
|
||||
"ar": "البريد الإلكتروني"
|
||||
},
|
||||
"subject": {
|
||||
"en": "Subject",
|
||||
"ar": "الموضوع"
|
||||
},
|
||||
"submit": {
|
||||
"en": "Submit",
|
||||
"ar": "أرسل"
|
||||
},
|
||||
"selectType": {
|
||||
"en": "Select Type",
|
||||
"ar": "أختر النوع"
|
||||
},
|
||||
"description": {
|
||||
"en": "Description",
|
||||
"ar": "الوصف"
|
||||
},
|
||||
"feedback": {
|
||||
"en": "Feedback",
|
||||
"ar": "تغذية راجعة"
|
||||
},
|
||||
"bug": {
|
||||
"en": "Bug",
|
||||
"ar": "بلاغ عن مشكلة"
|
||||
},
|
||||
"help": {
|
||||
"en": "Help",
|
||||
"ar": "طلب مساعدة"
|
||||
},
|
||||
"fieldRequired": {
|
||||
"en": "This field is required",
|
||||
"ar": "هذا الحقل إلزامي"
|
||||
},
|
||||
"invalidEmail": {
|
||||
"en": "Invalid email",
|
||||
"ar": "بريد إلكتروني خطأ"
|
||||
},
|
||||
"ticketSuccess": {
|
||||
"en": "Ticket submitted successfully!",
|
||||
"ar": "تم إرسال ملاحظتكم بنجاح"
|
||||
},
|
||||
"ticketError": {
|
||||
"en": "Failed to submit the ticket!",
|
||||
"ar": "فشل في إرسال الملاحظات"
|
||||
}
|
||||
}
|
||||
@@ -57,10 +57,6 @@
|
||||
"description": {
|
||||
"en": "Our algorithms provide speedy results and evaluate the test, providing a brief feedback on areas that are strong and the areas where improvement is needed. So there is no need to pay a hefty amount to a tutor and spend hours upon hours to review your performance. With EnCoach, you receive the evaluation within seconds.",
|
||||
"ar": "خوارزمياتنا توفر نتائج وتقييم سريع للاختبار، وتقدم ملاحظات موجزًة على الجوانب التي تتميز بها والجوانب التي تحتاج إلى تحسين. لذا ليس هناك حاجة لدفع مبلغ ضخم لمدرس وقضاء ساعات وساعات في مراجعة أدائك. مع إنكوتش، ستتلقى التقييم في غضون ثوانٍ."
|
||||
},
|
||||
"join": {
|
||||
"en": "Join",
|
||||
"ar": "انضم"
|
||||
}
|
||||
},
|
||||
"learn_more": {
|
||||
@@ -121,4 +117,4 @@
|
||||
"en": "Accreditation",
|
||||
"ar": "الاعتمادات الأكاديمية"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"ar": "منصة انكوتش"
|
||||
},
|
||||
"join": {
|
||||
"en": "Join",
|
||||
"en": "Sign up",
|
||||
"ar": "انضم إلينا"
|
||||
},
|
||||
"country_manager": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"joinus": {
|
||||
"en": "Join us",
|
||||
"en": "Sign up",
|
||||
"ar": "انضم إلينا"
|
||||
},
|
||||
"title": {
|
||||
@@ -22,5 +22,39 @@
|
||||
"packageIncludesC": {
|
||||
"en": "Allow yourself to correctly prepare for the exam",
|
||||
"ar": "امنح نفسك الفرصة للتحضير بشكل صحيح للإختبار"
|
||||
},
|
||||
"days": {
|
||||
"singular": {
|
||||
"en": "Day",
|
||||
"ar": "يوم"
|
||||
},
|
||||
"plural": {
|
||||
"en": "Days",
|
||||
"ar": "أيام"
|
||||
}
|
||||
},
|
||||
"weeks": {
|
||||
"singular": {
|
||||
"en": "Week",
|
||||
"ar": "أسبوع"
|
||||
},
|
||||
"plural": {
|
||||
"en": "Weeks",
|
||||
"ar": "أسابيع"
|
||||
}
|
||||
},
|
||||
"months": {
|
||||
"singular": {
|
||||
"en": "Month",
|
||||
"ar": "شهر"
|
||||
},
|
||||
"plural": {
|
||||
"en": "Months",
|
||||
"ar": "شهور"
|
||||
}
|
||||
},
|
||||
"encoach": {
|
||||
"en": "EnCoach",
|
||||
"ar": "إنكوتش"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user