├── .eslintrc.json ├── public ├── logo.webp ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── icons8-checkmark.svg ├── 998.svg └── 195.svg ├── postcss.config.js ├── next.config.js ├── app ├── utilities │ ├── CreateAnimation.tsx │ ├── FetchForms.tsx │ ├── VerifyCaptcha.tsx │ ├── SaveFormToDb.tsx │ └── SendEmail.tsx ├── contexts │ ├── AuthSessionProvider.tsx │ ├── ThemeProvider.tsx │ ├── QueryProvider.tsx │ ├── RecaptchaProvider.tsx │ ├── LanguageProvider.tsx │ └── LanguageContext.tsx ├── lib │ ├── types │ │ └── next-auth.d.ts │ ├── jwt.ts │ ├── ddb-toolbox.ts │ └── zod.tsx ├── hooks │ ├── useLanguageContext.tsx │ ├── useFooter.tsx │ ├── useThemeChanger.tsx │ ├── usePageIntroduction.tsx │ ├── useFootnotes.tsx │ ├── useCofounder.tsx │ ├── useQA.tsx │ ├── useAdminMenu.tsx │ ├── useHamburger.tsx │ ├── useServicePlan.tsx │ ├── useAdminLogin.tsx │ ├── useHeader.tsx │ ├── useLanguageDropdown.tsx │ └── useHeliosForms.tsx ├── globals.css ├── components │ ├── PageStyler.tsx │ ├── Spinner.tsx │ ├── Logo.tsx │ ├── SecondaryButton.tsx │ ├── Footnotes.tsx │ ├── MaintenanceView.tsx │ ├── ThemeChanger.tsx │ ├── PrimaryButton.tsx │ ├── PageIntroduction.tsx │ ├── CalendlyWidget.tsx │ ├── FeatureCard.tsx │ ├── MovingSlider.tsx │ ├── Cofounder.tsx │ ├── QA.tsx │ ├── LanguageDropdown.tsx │ ├── AdminTable.tsx │ ├── AdminLogin.tsx │ ├── BasicModal.tsx │ ├── ServicePlan.tsx │ ├── Hamburger.tsx │ ├── AdminMenu.tsx │ ├── Header.tsx │ ├── Footer.tsx │ ├── DataRecordsTable.tsx │ ├── ContactForm.tsx │ ├── HomeClient.tsx │ └── ApplicationForm.tsx ├── page.tsx ├── admin │ ├── page.tsx │ └── dashboard │ │ └── page.tsx ├── loading.tsx ├── entities │ ├── UserDDB.ts │ ├── ContactFormDDB.ts │ └── AplicationFormDDB.ts ├── apply │ └── page.tsx ├── api │ ├── login │ │ └── route.tsx │ ├── create-user │ │ └── route.tsx │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.tsx │ └── submit-form │ │ └── route.tsx ├── contact │ └── page.tsx ├── faq │ └── page.tsx ├── about │ └── page.tsx ├── layout.tsx └── services │ └── page.tsx ├── .gitignore ├── tsconfig.json ├── middleware.ts ├── README.md ├── tailwind.config.js └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangoz94/nextjs-education-consulting-website/HEAD/public/logo.webp -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangoz94/nextjs-education-consulting-website/HEAD/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangoz94/nextjs-education-consulting-website/HEAD/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangoz94/nextjs-education-consulting-website/HEAD/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangoz94/nextjs-education-consulting-website/HEAD/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangoz94/nextjs-education-consulting-website/HEAD/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangoz94/nextjs-education-consulting-website/HEAD/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | serverActions: true, 6 | }, 7 | }; 8 | 9 | module.exports = nextConfig; 10 | -------------------------------------------------------------------------------- /app/utilities/CreateAnimation.tsx: -------------------------------------------------------------------------------- 1 | import { gsap } from "gsap"; 2 | export default function createAnimation(target: HTMLElement, duration: number, properties: gsap.TweenVars) { 3 | return gsap.to(target, { 4 | duration, 5 | ...properties, 6 | }); 7 | }; -------------------------------------------------------------------------------- /public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /app/contexts/AuthSessionProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { SessionProvider } from "next-auth/react"; 3 | export default function AuthSessionProvider({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /app/lib/types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth/next"; 2 | 3 | 4 | declare module "next-auth" { 5 | interface Session { 6 | user: { 7 | id: string; 8 | username: string; 9 | password: string; 10 | accessToken: string; 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/contexts/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider } from "next-themes"; 4 | import { ReactNode } from "react"; 5 | 6 | export default function ThemeProviders({ children }: { children: ReactNode }){ 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /app/hooks/useLanguageContext.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { LanguageContext, LanguageContextValue } from "../contexts/LanguageContext"; 3 | 4 | export function useLanguageContext(): LanguageContextValue { 5 | const context = useContext(LanguageContext); 6 | 7 | if (!context) { 8 | throw new Error("useLanguageContext must be used within a LanguageProvider"); 9 | } 10 | 11 | return context; 12 | } -------------------------------------------------------------------------------- /app/contexts/QueryProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from "react"; 3 | import {QueryClient, QueryClientProvider} from "react-query"; 4 | 5 | export default function QueryProvider({children}: {children: React.ReactNode}) { 6 | const queryClient = new QueryClient(); 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | .pause { 7 | animation-play-state: paused; 8 | } 9 | } 10 | 11 | html, 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | box-sizing: border-box; 16 | } 17 | 18 | main { 19 | height: 100%; 20 | min-height: calc(100dvh - 12rem); 21 | } 22 | 23 | .grecaptcha-badge { 24 | visibility: hidden; 25 | } 26 | -------------------------------------------------------------------------------- /app/hooks/useFooter.tsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useRef } from "react"; 2 | import { gsap } from "gsap"; 3 | 4 | export function useFooter() { 5 | const footerRef = useRef(null); 6 | 7 | useLayoutEffect(() => { 8 | // gsap animations 9 | gsap.from(footerRef.current, { 10 | opacity: 0, 11 | duration: 0.25, 12 | ease: "ease-in-out", 13 | }); 14 | }, []); 15 | 16 | return { footerRef }; 17 | } 18 | -------------------------------------------------------------------------------- /app/components/PageStyler.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function PageStyler({ 4 | children, 5 | optionalClassNames, 6 | }: { 7 | children: React.ReactNode; 8 | optionalClassNames?: string; 9 | }) { 10 | return ( 11 |
14 | {children} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import HomeClient from "./components/HomeClient"; 2 | import PageStyler from "./components/PageStyler"; 3 | 4 | export const metadata = { 5 | title: "Homepage | Anasayfa", 6 | description: "Homepage | Anasayfa", 7 | }; 8 | 9 | export default function Home() { 10 | return ( 11 | 12 | {/* HomeClient is only a wrapper to avoid using 'use client' in page component for SEO purposes */} 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/hooks/useThemeChanger.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export default function useThemeChanger() { 5 | const [mounted, setMounted] = useState(false); 6 | const { theme, setTheme } = useTheme(); 7 | 8 | useEffect(() => { 9 | setMounted(true); 10 | }, []); 11 | 12 | if (!mounted) { 13 | return null; 14 | } 15 | 16 | const light = theme === "light"; 17 | 18 | return {light, setTheme} 19 | } 20 | -------------------------------------------------------------------------------- /app/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminLogin from "../components/AdminLogin"; 2 | import PageStyler from "../components/PageStyler"; 3 | import type { Metadata } from "next"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Admin Login | Admin Girişi", 7 | description: "Admin Page", 8 | }; 9 | 10 | export default function Admin() { 11 | return ( 12 | 13 |
14 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/hooks/usePageIntroduction.tsx: -------------------------------------------------------------------------------- 1 | import { LegacyRef, MutableRefObject, useLayoutEffect, useRef } from "react"; 2 | import { gsap } from "gsap"; 3 | 4 | export function usePageIntroduction() { 5 | const pageIntroDivRef = useRef(null); 6 | 7 | useLayoutEffect(() => { 8 | // GSAP Animations 9 | gsap.from(pageIntroDivRef.current!, { 10 | y: -100, 11 | opacity: 0, 12 | duration: 0.5, 13 | ease: "power3.out", 14 | }) 15 | }, []); 16 | 17 | return { pageIntroDivRef }; 18 | } 19 | -------------------------------------------------------------------------------- /app/hooks/useFootnotes.tsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useRef } from "react"; 2 | import { gsap } from "gsap"; 3 | 4 | export function useFootnotes() { 5 | const footnotesRef = useRef(null); 6 | 7 | useLayoutEffect(() => { 8 | gsap.from(footnotesRef.current, { 9 | opacity: 0, 10 | duration: 1, 11 | ease: "power3.out", 12 | scrollTrigger: { 13 | trigger: footnotesRef.current, 14 | start: "top 90%", 15 | }, 16 | }); 17 | }, []); 18 | 19 | return { footnotesRef }; 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | *.env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | .vscode 38 | -------------------------------------------------------------------------------- /app/admin/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminMenu from "@/app/components/AdminMenu"; 2 | import PageStyler from "@/app/components/PageStyler"; 3 | import type { Metadata } from "next"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Admin Dashboard | Admin Paneli", 7 | description: "Admin-only dashboard | Sadece yöneticilerin görebileceği panel", 8 | }; 9 | 10 | export default function Dashboard() { 11 | return ( 12 | 13 |
14 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PageStyler from "./components/PageStyler"; 3 | 4 | const Loading = () => { 5 | return ( 6 | 7 |
8 | {Array.from({ length: 3 }, (_, i) => ( 9 |
14 | ))} 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Loading; 21 | -------------------------------------------------------------------------------- /app/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Spinner() { 4 | return ( 5 |
6 |
10 | 11 | Loading... 12 | 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/entities/UserDDB.ts: -------------------------------------------------------------------------------- 1 | import { Entity, EntityItem } from "dynamodb-toolbox"; 2 | import { MyTable } from "../lib/ddb-toolbox"; 3 | 4 | export const UserEntity = new Entity({ 5 | name: 'User', 6 | timestamps: true, 7 | attributes: { 8 | PK: { partitionKey: true, default: (data:any) => `USER#${data.username}`, dependsOn: 'username' }, 9 | SK: { sortKey: true, default: () => `METADATA` }, 10 | username: { type: 'string', required: true }, 11 | password: { type: 'string', required: true }, 12 | }, 13 | table: MyTable 14 | } as const); 15 | 16 | 17 | export type UserDDB = EntityItem -------------------------------------------------------------------------------- /app/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | 5 | type LogoProps = { 6 | setActiveLink: React.Dispatch>; 7 | }; 8 | 9 | export default function Logo(props: LogoProps) { 10 | return ( 11 |
12 | props.setActiveLink("/")}> 13 |

14 | Helios Admissions 15 |

16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/hooks/useCofounder.tsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useRef } from "react"; 2 | import gsap from "gsap"; 3 | import ScrollTrigger from "gsap/dist/ScrollTrigger"; 4 | 5 | gsap.registerPlugin(ScrollTrigger); 6 | 7 | export function useCofounder() { 8 | const cofounderRef = useRef(null); 9 | 10 | useLayoutEffect(() => { 11 | gsap.from(cofounderRef.current, { 12 | duration: 1, 13 | y: 100, 14 | opacity: 0, 15 | ease: "power3.out", 16 | scrollTrigger: { 17 | trigger: cofounderRef.current, 18 | start: "top 95%", 19 | }, 20 | }); 21 | }, []); 22 | return { cofounderRef }; 23 | } 24 | -------------------------------------------------------------------------------- /app/components/SecondaryButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useLanguageContext } from "../hooks/useLanguageContext"; 3 | 4 | type SecondaryButtonProps = { 5 | label: { 6 | en: string; 7 | tr: string; 8 | }; 9 | onClick: () => void; 10 | }; 11 | 12 | export default function SecondaryButton(props: SecondaryButtonProps) { 13 | const { language } = useLanguageContext(); 14 | return ( 15 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /public/icons8-checkmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/hooks/useQA.tsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useRef, useState } from "react"; 2 | import { useLanguageContext } from "./useLanguageContext"; 3 | import { gsap } from "gsap"; 4 | export default function useQA() { 5 | useLayoutEffect(() => { 6 | // GSAP Animations 7 | 8 | gsap.fromTo( 9 | ".gsap-qa", 10 | { 11 | opacity: 0, 12 | y: 100, 13 | }, 14 | { 15 | opacity: 1, 16 | y: 0, 17 | stagger: 0.08, 18 | ease: "easeInOut", 19 | } 20 | ); 21 | }, []); 22 | 23 | const [isOpen, setIsOpen] = useState(false); 24 | const language = useLanguageContext().language; 25 | 26 | return { isOpen, setIsOpen, language }; 27 | } 28 | -------------------------------------------------------------------------------- /app/components/Footnotes.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { useLanguageContext } from "../hooks/useLanguageContext"; 4 | 5 | interface FootnotesProps { 6 | footnotes: { 7 | en: string; 8 | tr: string; 9 | }[]; 10 | } 11 | 12 | export default function Footnotes({ footnotes }: FootnotesProps) { 13 | const language = useLanguageContext().language; 14 | return ( 15 |
16 |
    17 | {footnotes.map((footnote, index) => ( 18 |
  1. 19 | {language === "en" ? footnote.en : footnote.tr} 20 |
  2. 21 | ))} 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/utilities/FetchForms.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { ContactFormDDB, ContactFormEntity } from "../entities/ContactFormDDB"; 3 | import { ApplicationFormDDB, ApplicationFormEntity } from "../entities/AplicationFormDDB"; 4 | // server actions - removing use server will throw an error 5 | export async function fetchContactForms() { 6 | const contactForms = (await ContactFormEntity.query('ContactForm',{ 7 | reverse:true 8 | } 9 | )).Items 10 | 11 | return contactForms as ContactFormDDB[] 12 | } 13 | 14 | export async function fetchApplicationForms() { 15 | const applicationForms = (await ApplicationFormEntity.query('ApplicationForm',{ 16 | reverse:true, 17 | })).Items 18 | 19 | return applicationForms as ApplicationFormDDB[] 20 | } 21 | -------------------------------------------------------------------------------- /app/contexts/RecaptchaProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | //This component is just a wrapper to avoid using 'use client' in contact page for SEO purposes. 3 | import React from "react"; 4 | import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3"; 5 | import { useLanguageContext } from "../hooks/useLanguageContext"; 6 | 7 | interface RecaptchaProviderProps { 8 | children: React.ReactNode; 9 | } 10 | 11 | export default function RecaptchaProvider({ 12 | children, 13 | }: RecaptchaProviderProps) { 14 | const language = useLanguageContext().language; 15 | 16 | return ( 17 | 21 | {children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/components/MaintenanceView.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function MaintenanceView() { 4 | return ( 5 |
6 |

Site No Longer Live

7 |

8 | This project was created purely for learning and demonstration purposes. It is a fictional project and is no longer being maintained. 9 |

10 |

If you are interested in experimenting or deploying it yourself, feel free to clone the repository and set up the necessary environment variables and set up the placeholder content as needed.

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | ".next/types/**/*.ts", 31 | "app/api/submit-contact-form" 32 | ], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /app/components/ThemeChanger.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { BsMoonStarsFill, BsFillSunFill } from "react-icons/bs"; 5 | import useThemeChanger from "../hooks/useThemeChanger"; 6 | 7 | export default function ThemeChanger() { 8 | const themeChanger = useThemeChanger(); 9 | if (!themeChanger) { 10 | return null; 11 | } 12 | const { light, setTheme } = themeChanger; 13 | return ( 14 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/entities/ContactFormDDB.ts: -------------------------------------------------------------------------------- 1 | import { Entity, EntityItem } from "dynamodb-toolbox"; 2 | import { MyTable } from "../lib/ddb-toolbox"; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | export const ContactFormEntity = new Entity({ 6 | name: 'ContactForm', 7 | timestamps: true, 8 | attributes: { 9 | PK: { partitionKey: true, default: `ContactForm` }, 10 | SK: { sortKey: true, default: (data:{id:string}) => `ID#${data.id}`, dependsOn: 'id' }, 11 | id: { type: 'string', required: true, default: () => uuidv4()}, 12 | fullName: { type: 'string', required: true }, 13 | email: { type: 'string', required: true }, 14 | phone: { type: 'string', required: true }, 15 | question: { type: 'string', required: true }, 16 | }, 17 | table: MyTable 18 | } as const); 19 | 20 | export type ContactFormDDB = EntityItem -------------------------------------------------------------------------------- /app/lib/jwt.ts: -------------------------------------------------------------------------------- 1 | import jwt, { JwtPayload } from "jsonwebtoken"; 2 | 3 | interface SignOption { 4 | expiresIn?: string; 5 | } 6 | 7 | const DEFAULT_SIGN_OPTIONS: SignOption = { 8 | expiresIn: "1d", 9 | }; 10 | 11 | export function signJwtAccessToken( 12 | payload: JwtPayload, 13 | options: SignOption = DEFAULT_SIGN_OPTIONS 14 | ): string { 15 | const secret_key = process.env.JWT_SECRET_KEY; 16 | if (!secret_key) { 17 | throw new Error("JWT_ACCESS_TOKEN_SECRET is not defined"); 18 | } 19 | const token = jwt.sign(payload, secret_key!, options); 20 | return token; 21 | } 22 | 23 | export function verifyJwt(token: string) { 24 | try { 25 | const secret_key = process.env.JWT_SECRET_KEY; 26 | const decoded = jwt.verify(token, secret_key!); 27 | return decoded as JwtPayload; 28 | } catch (err) { 29 | console.log(err); 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/components/PrimaryButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { ButtonHTMLAttributes, MouseEventHandler } from "react"; 3 | import { useLanguageContext } from "../hooks/useLanguageContext"; 4 | 5 | interface PrimaryButtonProps extends ButtonHTMLAttributes { 6 | label: { en: string; tr: string }; 7 | className?: string; 8 | } 9 | 10 | export default function PrimaryButton({ ...props }: PrimaryButtonProps) { 11 | const language = useLanguageContext().language; 12 | return ( 13 |
14 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/hooks/useAdminMenu.tsx: -------------------------------------------------------------------------------- 1 | import { ContactFormDDB } from "../entities/ContactFormDDB"; 2 | import { ApplicationFormDDB } from "../entities/AplicationFormDDB"; 3 | 4 | import { useState } from "react"; 5 | import { useQuery } from "react-query"; 6 | import { 7 | fetchApplicationForms, 8 | fetchContactForms, 9 | } from "../utilities/FetchForms"; 10 | 11 | export function useAdminMenu() { 12 | const [openTab, setOpenTab] = useState(1); 13 | 14 | const { data: allContactForms, isLoading: isContactFormLoading } = useQuery("contactForms", fetchContactForms); 15 | const { data: allApplicationForms, isLoading: isApplicationFormLoading } = useQuery("applicationForms", fetchApplicationForms); 16 | return { 17 | openTab, 18 | setOpenTab, 19 | allContactForms, 20 | allApplicationForms, 21 | isLoading: isContactFormLoading || isApplicationFormLoading, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /app/hooks/useHamburger.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { useLanguageContext } from "./useLanguageContext"; 3 | 4 | export function useHamburger() { 5 | const [isHamburger, setIsHamburger] = useState(false); 6 | const language = useLanguageContext().language; 7 | const hamburgerRef = useRef(null); 8 | 9 | const handleClickOutside = (event: MouseEvent) => { 10 | if ( 11 | hamburgerRef.current && 12 | !hamburgerRef.current.contains(event.target as Node) 13 | ) { 14 | 15 | setIsHamburger(false); 16 | } 17 | }; 18 | 19 | useEffect(() => { 20 | document.addEventListener("click", handleClickOutside); 21 | return () => { 22 | document.removeEventListener("click", handleClickOutside); 23 | }; 24 | }, []); 25 | 26 | return { 27 | hamburgerRef, 28 | isHamburger, 29 | setIsHamburger, 30 | language, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /app/utilities/VerifyCaptcha.tsx: -------------------------------------------------------------------------------- 1 | export async function verifyRecaptcha(token: string): Promise { 2 | try { 3 | const secret = process.env.RECAPTCHA_SECRET as string; 4 | const recaptchaVerificationResponse = await fetch( 5 | `https://www.google.com/recaptcha/api/siteverify?secret=${secret}&response=${token}`, 6 | { 7 | method: "POST", 8 | headers: { 9 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 10 | }, 11 | } 12 | ); 13 | const recaptchaVerificationResponseData = 14 | await recaptchaVerificationResponse.json(); 15 | console.log(recaptchaVerificationResponseData); 16 | if ( 17 | recaptchaVerificationResponseData.success && 18 | recaptchaVerificationResponseData.score > 0.7 19 | ) { 20 | return true; 21 | } 22 | return false; 23 | } catch (error) { 24 | console.error(error); 25 | throw new Error("Failed to verify recaptcha token"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/contexts/LanguageProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useState, ReactNode, useMemo, useEffect } from "react"; 3 | import { LanguageContext } from "./LanguageContext"; 4 | 5 | interface LanguageProviderProps { 6 | children: ReactNode; 7 | } 8 | 9 | function LanguageProvider({ children }: LanguageProviderProps) { 10 | const [language, setLanguage] = useState('en'); 11 | 12 | // Get the language from localStorage on first render 13 | useEffect(() => { 14 | const storedLanguage = localStorage.getItem('language'); 15 | if (storedLanguage) { 16 | setLanguage(storedLanguage); 17 | } 18 | }, []); 19 | 20 | const value = useMemo(() => ({ language, setLanguage }), [language]); 21 | 22 | // Update the localStorage value whenever the language changes 23 | useEffect(() => { 24 | localStorage.setItem('language', language); 25 | }, [language]); 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | ); 32 | } 33 | 34 | export { LanguageProvider }; -------------------------------------------------------------------------------- /app/components/PageIntroduction.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useLanguageContext } from "../hooks/useLanguageContext"; 3 | import { usePageIntroduction } from "../hooks/usePageIntroduction"; 4 | 5 | type PageIntroductionProps = { 6 | title: { 7 | en: string; 8 | tr: string; 9 | }; 10 | description: { 11 | en: string; 12 | tr: string; 13 | }; 14 | }; 15 | 16 | export default function PageIntroduction({ 17 | title, 18 | description, 19 | }: PageIntroductionProps) { 20 | const { language } = useLanguageContext(); 21 | const { pageIntroDivRef } = usePageIntroduction(); 22 | return ( 23 |
27 |

28 | {language === "en" ? title.en : title.tr} 29 |

30 |

31 | {language === "en" ? description.en : description.tr} 32 |

33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/lib/ddb-toolbox.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDB, DynamoDBClient } from "@aws-sdk/client-dynamodb"; 2 | import { Table, Entity } from "dynamodb-toolbox"; 3 | import { STS } from "@aws-sdk/client-sts"; 4 | import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; 5 | 6 | const marshallOptions = { 7 | // Specify your client options as usual 8 | convertEmptyValues: false, 9 | }; 10 | 11 | const translateConfig = { marshallOptions }; 12 | 13 | const sts = new STS({ 14 | region: "us-east-1", 15 | }); 16 | 17 | export const DocumentClient = DynamoDBDocumentClient.from( 18 | new DynamoDBClient({ 19 | region: "us-east-1", 20 | credentials: { 21 | accessKeyId: process.env.AWS_ACCESS_KEY_ID as string, 22 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string, 23 | }, 24 | }), 25 | translateConfig 26 | ); 27 | 28 | export const MyTable = new Table({ 29 | // Specify table name (used by DynamoDB) 30 | name: "projects-mix-ddb", 31 | 32 | // Define partition and sort keys 33 | partitionKey: "PK", 34 | sortKey: "SK", 35 | 36 | // Add the DocumentClient 37 | DocumentClient, 38 | }); 39 | -------------------------------------------------------------------------------- /app/apply/page.tsx: -------------------------------------------------------------------------------- 1 | import PageStyler from "../components/PageStyler"; 2 | import ApplicationForm from "../components/ApplicationForm"; 3 | import RecaptchaProvider from "../contexts/RecaptchaProvider"; 4 | import PageIntroduction from "../components/PageIntroduction"; 5 | import type { Metadata } from "next"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Apply | Başvur", 9 | description: "Apply | Başvur", 10 | }; 11 | 12 | export default function Apply() { 13 | return ( 14 | 15 | 16 |
17 | 27 | 28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/hooks/useServicePlan.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import gsap from "gsap"; 3 | import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; 4 | 5 | gsap.registerPlugin(ScrollTrigger); 6 | 7 | export function useServicePlan() { 8 | const hRef = useRef(null); 9 | const pRef = useRef(null); 10 | const spanRef = useRef(null); 11 | const uRef = useRef(null); 12 | 13 | useEffect(() => { 14 | // Create an array of refs and a delay value 15 | const refs = [hRef, pRef, spanRef, uRef]; 16 | const delay = 0.1; 17 | 18 | // Loop through the refs and animate them with a staggered delay 19 | refs.forEach((ref, index) => { 20 | gsap.from(ref.current, { 21 | y: 100, 22 | opacity: 0, 23 | duration: 0.5, 24 | ease: "power3.out", 25 | delay: index * delay, // Increase the delay by the index 26 | scrollTrigger: { 27 | trigger: ref.current, 28 | start: "top 105%", 29 | toggleActions: "play none none reverse", 30 | }, 31 | }); 32 | }); 33 | }, []); 34 | 35 | return { hRef, pRef, spanRef, uRef }; 36 | } 37 | -------------------------------------------------------------------------------- /app/components/CalendlyWidget.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import Link from "next/link"; 4 | import { useLanguageContext } from "../hooks/useLanguageContext"; 5 | import { BsFillCalendarCheckFill } from "react-icons/bs"; 6 | 7 | export default function CalendlyWidget() { 8 | const { language } = useLanguageContext(); 9 | const calendlyLink = "https://calendly.com/helios-admissions/45min"; 10 | 11 | const handleCalendlyClick = () => { 12 | window.open(calendlyLink, "_blank"); 13 | }; 14 | 15 | return ( 16 |
17 | 22 | {language === "en" ? "Book A Call" : "Randevu Al"} 23 | 24 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getToken } from "next-auth/jwt"; 3 | 4 | export async function middleware(req: NextRequest) { 5 | const { pathname } = new URL(req.url, process.env.NEXTAUTH_URL); 6 | 7 | // Check if the user is logged in 8 | const session = await getToken({ req, secret: process.env.NEXTAUTH_SECRET }); 9 | 10 | if (pathname === "/admin") { 11 | // Redirect user to /admin/dashboard if logged in 12 | if (session) { 13 | return NextResponse.redirect(process.env.NEXTAUTH_URL + "/admin/dashboard"); 14 | } 15 | } else if (pathname === "/admin/dashboard") { 16 | // Check if the user is logged in, otherwise redirect to /admin 17 | if (!session) { 18 | return NextResponse.redirect(process.env.NEXTAUTH_URL + "/admin"); 19 | } 20 | } 21 | 22 | // If the user is logged in and accessing /admin/dashboard, continue to the requested page 23 | if (session && pathname.startsWith("/admin/dashboard")) { 24 | return NextResponse.next(); 25 | } 26 | 27 | // If none of the conditions above match, continue to the requested page 28 | return NextResponse.next(); 29 | } 30 | -------------------------------------------------------------------------------- /app/api/login/route.tsx: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import bcrypt from "bcrypt"; 3 | import { signJwtAccessToken } from "@/app/lib/jwt"; 4 | import { UserEntity } from "@/app/entities/UserDDB"; 5 | 6 | export async function POST(request: NextRequest) { 7 | const body = await request.json(); 8 | const { username, password } = body; 9 | console.log(await bcrypt.hash(password, 10)); 10 | 11 | const user = (await UserEntity.get({ PK: `USER#${username}`, SK: "METADATA" })).Item; 12 | if (user && (await bcrypt.compare(password, user.password))) { 13 | const { password, ...userWithoutPass } = user; 14 | const accessToken = signJwtAccessToken(userWithoutPass); 15 | const result = { 16 | ...userWithoutPass, 17 | accessToken, 18 | }; 19 | return new NextResponse(JSON.stringify(result ), { 20 | status: 200, 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | }); 25 | } else { 26 | return new NextResponse(JSON.stringify({message: "Invalid Credentials"}), { 27 | status: 401, 28 | headers: { 29 | "Content-Type": "application/json", 30 | }, 31 | 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [ARCHIVED] 2 | 3 | # Disclaimer 4 | This is NOT an actual project live / in production. That's why the source code is publicly available and it is NOT actively maintained. 5 | This repository and website are part of a project template created for demonstration, learning, and prototyping purposes only. 6 | All website data, design elements, and configurations (including AWS resources and environment variables) are mock or placeholder examples and do not represent a production application. 7 | 8 | # Tech Stack 9 | Next.js 13, Next Auth, React,Typescript, React Query, React Hook Forms, Zod, Tailwind CSS, ~~Prisma, PostgreSQL, AWS RDS~~, DynamoDB and Dynamo DB Toolbox 10 | 11 | AWS RDS, Postgres and Prisma were replaced with DynamoDB and Dynamo DB Toolbox as part of cost optimizations as of Dec 26, 2023. 12 | 13 | # Website URL 14 | As this project is for demonstration purposes only, it is not deployed/live. However, it can be deployed easily with the following env vars. 15 | 16 | # Env Variables 17 | NEXTAUTH_URL= http://localhost:3000 18 | NEXTAUTH_SECRET= 19 | NEXT_PUBLIC_RECAPTCHA_PUBLIC 20 | RECAPTCHA_SECRET 21 | SENDGRID_API_KEY 22 | JWT_SECRET_KEY= 23 | AWS_ACCESS_KEY_ID= 24 | AWS_SECRET_ACCESS_KEY= 25 | -------------------------------------------------------------------------------- /app/contact/page.tsx: -------------------------------------------------------------------------------- 1 | import PageStyler from "../components/PageStyler"; 2 | import ContactForm from "../components/ContactForm"; 3 | import PageIntroduction from "../components/PageIntroduction"; 4 | import RecaptchaProvider from "../contexts/RecaptchaProvider"; 5 | import type { Metadata } from "next"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Contact | İletişim", 9 | description: "Contact us | Bize ulaşın", 10 | }; 11 | 12 | export default function Contact() { 13 | const CONTACT_INTRODUCTION_DATA = { 14 | title: { 15 | en: "Contact Form", 16 | tr: "İletişim Formu", 17 | }, 18 | description: { 19 | en: "Please fill out the form below and we will get back to you as soon as possible.", 20 | tr: "Aşağıdaki formu doldurup bize ulaşabilirsiniz. En kısa sürede size geri dönüş yapacağız.", 21 | }, 22 | }; 23 | return ( 24 | 25 | 26 |
27 | 31 | 32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/components/FeatureCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import { useLanguageContext } from "../hooks/useLanguageContext"; 4 | 5 | type FeatureProps = { 6 | title: { 7 | en: string; 8 | tr: string; 9 | }; 10 | description: { 11 | en: string; 12 | tr: string; 13 | }; 14 | style?: string; 15 | }; 16 | 17 | export default function FeatureCard(props: FeatureProps) { 18 | const { language } = useLanguageContext(); 19 | 20 | return ( 21 |
24 | a checkmark icon 35 |
36 |

37 | {language === "en" ? props.title.en : props.title.tr} 38 |

39 |

40 | {language === "en" ? props.description.en : props.description.tr} 41 |

42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/contexts/LanguageContext.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { setCookie, parseCookies } from 'nookies'; 3 | import { createContext, useState, ReactNode, useMemo, useEffect } from "react"; 4 | 5 | export interface LanguageContextValue { 6 | language: string; 7 | setLanguage: (language: string) => void; 8 | } 9 | 10 | const LanguageContext = createContext( 11 | undefined 12 | ); 13 | 14 | interface LanguageProviderProps { 15 | children: ReactNode; 16 | } 17 | 18 | function LanguageProvider({ children }: LanguageProviderProps) { 19 | const [language, setLanguage] = useState('en'); 20 | // Get the language from the cookie on first render 21 | let storedLanguage = "en"; 22 | if (typeof window !== "undefined") { 23 | const cookies = parseCookies(); 24 | storedLanguage = cookies.language || "en"; 25 | } 26 | const value = useMemo(() => ({ language, setLanguage }), [language]); 27 | 28 | // Update the cookie value whenever the language changes 29 | useEffect(() => { 30 | if (typeof window !== "undefined") { 31 | setCookie(null, 'language', language, { 32 | maxAge: 30 * 24 * 60 * 60, 33 | path: '/', 34 | }); 35 | } 36 | }, [language]); 37 | 38 | return ( 39 | 40 | {children} 41 | 42 | ); 43 | } 44 | 45 | export { LanguageProvider, LanguageContext }; -------------------------------------------------------------------------------- /app/hooks/useAdminLogin.tsx: -------------------------------------------------------------------------------- 1 | import { signIn, useSession } from "next-auth/react"; 2 | import { useRouter } from "next/navigation"; 3 | import { useRef } from "react"; 4 | 5 | export function useAdminLogin() { 6 | const usernameRef = useRef(""); 7 | const passwordRef = useRef(""); 8 | const { data: session } = useSession(); 9 | const router = useRouter(); 10 | 11 | const handleAdminLogin = async (event: { preventDefault: () => void }) => { 12 | event.preventDefault(); 13 | 14 | if (session && session.user) { 15 | // Admin already logged in (unlikely due to middleware redirect but just in case) 16 | console.log("Admin already logged in"); 17 | return; 18 | } 19 | 20 | try { 21 | const result = await signIn("credentials", { 22 | username: usernameRef.current, 23 | password: passwordRef.current, 24 | // if login is successful, redirect to /admin/dashboard if not dont redirect 25 | redirect: false, 26 | callbackUrl: "/admin/dashboard", 27 | }); 28 | 29 | if (result?.error) { 30 | alert("Invalid Credentials - Try again or contact admin"); 31 | } else { 32 | router.push("/admin/dashboard"); 33 | } 34 | 35 | console.log(result); 36 | return result; 37 | } catch (error: any) { 38 | throw new Error("Error while authenticating admin/user ", error); 39 | } 40 | }; 41 | 42 | return { usernameRef, passwordRef, handleAdminLogin }; 43 | } 44 | -------------------------------------------------------------------------------- /app/components/MovingSlider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useLanguageContext } from "../hooks/useLanguageContext"; 3 | 4 | type MovingSliderProps = { 5 | width: number; 6 | texts: { en: string; tr: string }[]; 7 | }; 8 | 9 | export default function MovingSlider(props: MovingSliderProps) { 10 | const { language } = useLanguageContext(); 11 | return ( 12 |
13 | {[...Array(2)].map((_, index) => ( 14 |
20 | {props.texts.map((text, index) => ( 21 |
25 |

26 | {language === "en" ? text.en : text.tr} 27 |

28 |
29 | ))} 30 |
31 | ))} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/components/Cofounder.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import { useLanguageContext } from "../hooks/useLanguageContext"; 4 | import { useCofounder } from "../hooks/useCofounder"; 5 | 6 | type CofounderProps = { 7 | name: string; 8 | imagePath: string; 9 | imageAlt: string; 10 | description: { 11 | en: string; 12 | tr: string; 13 | }; 14 | index: number; 15 | }; 16 | 17 | export default function Cofounder(props: CofounderProps) { 18 | const { language } = useLanguageContext(); 19 | const { cofounderRef } = useCofounder(); 20 | return ( 21 |
26 | {props.imageAlt} 27 |
28 |
29 |

{props.name}

30 |

{language === "en" ? "Co-founder" : "Kurucu"}

31 |
32 |
33 |

{language === "en" ? props.description.en : props.description.tr}

34 |
35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/components/QA.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ChevronDownIcon } from "@heroicons/react/solid"; 3 | import useQA from "../hooks/useQA"; 4 | 5 | type QAProps = { 6 | question: { 7 | en: string; 8 | tr: string; 9 | }; 10 | answer: { 11 | en: string; 12 | tr: string; 13 | }; 14 | order: number; 15 | }; 16 | 17 | export default function QA({ question, answer, order }: QAProps) { 18 | const { isOpen, setIsOpen, language } = useQA(); 19 | 20 | return ( 21 |
setIsOpen(!isOpen)} 24 | > 25 |
26 |

27 | {order}. 28 | {language === "en" ? question.en : question.tr} 29 |

30 | 35 |
36 | {isOpen && ( 37 |

38 | {language === "en" ? answer.en : answer.tr} 39 |

40 | )} 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/entities/AplicationFormDDB.ts: -------------------------------------------------------------------------------- 1 | import { Entity, EntityItem } from "dynamodb-toolbox"; 2 | import { MyTable } from "../lib/ddb-toolbox"; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | export const ApplicationFormEntity = new Entity({ 6 | name: 'ApplicationForm', 7 | timestamps: true, 8 | attributes: { 9 | PK: { partitionKey: true, default:`ApplicationForm` }, 10 | SK: { sortKey: true, default: (data:{id:string}) => `ID#${data.id}`, dependsOn: 'id' }, 11 | id: { type: 'string', required: true, default: () => uuidv4()}, 12 | package: { type: 'string', required: true }, 13 | programType: { type: 'string', required: true }, 14 | whyUSA: { type: 'string', required: true }, 15 | academicInterests: { type: 'string', required: true }, 16 | fullName: { type: 'string', required: true }, 17 | email: { type: 'string', required: true }, 18 | phone: { type: 'string', required: true }, 19 | citizenship: { type: 'string', required: true }, 20 | university: { type: 'string', required: true }, 21 | major: { type: 'string', required: true }, 22 | gpa: { type: 'string', required: true }, 23 | extracurricular: { type: 'string', required: true }, 24 | workExperience: { type: 'string', required: true }, 25 | englishProficiency: { type: 'string', required: true }, 26 | toeflIelts: { type: 'string', required: true }, 27 | gre: { type: 'string', required: true }, 28 | }, 29 | table: MyTable 30 | } as const); 31 | 32 | export type ApplicationFormDDB = EntityItem -------------------------------------------------------------------------------- /app/components/LanguageDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { useLanguageDropdown } from "../hooks/useLanguageDropdown"; 2 | 3 | export function LanguageDropdown() { 4 | const { 5 | context, 6 | LANGUAGES, 7 | isDropDownOpen, 8 | setIsDropDownOpen, 9 | handleOptionClick, 10 | dropdownRef, 11 | } = useLanguageDropdown(); 12 | 13 | return ( 14 |
18 | 25 | {isDropDownOpen && ( 26 |
    30 | {LANGUAGES.map((language) => ( 31 |
  • 36 | {language.name} 37 |
  • 38 | ))} 39 |
40 | )} 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/components/AdminTable.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | TableHead, 4 | TableRow, 5 | TableHeaderCell, 6 | TableBody, 7 | TableCell, 8 | Text, 9 | } from "@tremor/react"; 10 | import { ContactFormDDB } from "../entities/ContactFormDDB"; 11 | 12 | export default function AdminTable({ 13 | data, 14 | }: { 15 | data: ContactFormDDB[] | undefined; 16 | }) { 17 | return ( 18 | 19 | 20 | 21 | # 22 | Full Name 23 | Email 24 | Phone 25 | Question 26 | 27 | 28 | 29 | {data?.map((data, index) => ( 30 | 31 | 32 | {index + 1} 33 | 34 | 35 | {data.fullName} 36 | 37 | 38 | {data.email} 39 | 40 | 41 | {data.phone} 42 | 43 | 44 | {data.question} 45 | 46 | 47 | ))} 48 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/utilities/SaveFormToDb.tsx: -------------------------------------------------------------------------------- 1 | import { ApplicationFormData, ContactFormData } from "../lib/zod"; 2 | import { ApplicationFormEntity } from "../entities/AplicationFormDDB"; 3 | import { ContactFormEntity } from "../entities/ContactFormDDB"; 4 | 5 | export async function saveFormToDb( 6 | formData: ApplicationFormData | ContactFormData, 7 | formType: "application" | "contact" 8 | ): Promise { 9 | try { 10 | let data; 11 | if (formType === "application" && "package" in formData) { 12 | data = { 13 | package: formData.package, 14 | programType: formData.programType, 15 | whyUSA: formData.whyUSA, 16 | academicInterests: formData.academicInterests, 17 | fullName: formData.fullName, 18 | email: formData.email, 19 | phone: formData.phone, 20 | citizenship: formData.citizenship, 21 | university: formData.university, 22 | major: formData.major, 23 | gpa: formData.gpa, 24 | extracurricular: formData.extracurricular, 25 | workExperience: formData.workExperience, 26 | englishProficiency: formData.englishProficiency, 27 | toeflIelts: formData.toeflIelts, 28 | gre: formData.gre, 29 | }; 30 | const form = await ApplicationFormEntity.put(data); 31 | console.log(form); 32 | } else if (formType === "contact" && "question" in formData) { 33 | data = { 34 | fullName: formData.fullName, 35 | email: formData.email, 36 | phone: formData.phone, 37 | question: formData.question, 38 | }; 39 | const form = await ContactFormEntity.put(data); 40 | console.log(form); 41 | } 42 | } catch (error) { 43 | console.log(error); 44 | throw new Error("Error saving form to database"); 45 | } 46 | return true; 47 | } 48 | -------------------------------------------------------------------------------- /public/998.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | mode: "jit", 4 | purge: ["./components/**/*.{js,ts,jsx,tsx}", "./app/**/*.{js,ts,jsx,tsx}"], 5 | darkMode: "class", 6 | content: [ 7 | "./pages/**/*.{js,ts,jsx,tsx}", 8 | "./components/**/*.{js,ts,jsx,tsx}", 9 | "./app/**/*.{js,ts,jsx,tsx}", 10 | ], 11 | theme: { 12 | extend: { 13 | backgroundImage: { 14 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 15 | "gradient-conic": 16 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 17 | "gradient-pseudo-left": 18 | "linear-gradient(to left, rgba(242,236,228,0) 0%, rgba(242,236,228,1) 100%)", 19 | "gradient-pseudo-right": 20 | "linear-gradient(to right, rgba(242,236,228,0) 0%, rgba(242,236,228,1) 100%)", 21 | "gradient-pseudo-right-dark": 22 | "linear-gradient(to right, rgba(17,24,39,0) 0%, rgba(17,24,39,1) 100%)", 23 | "gradient-pseudo-left-dark": 24 | "linear-gradient(to left, rgba(17,24,39,0) 0%, rgba(17,24,39,1) 100%)", 25 | }, 26 | backgroundColor: { 27 | LIGHT_PRIMARY_BG_COLOR: "#f2ece4", 28 | LIGHT_SECONDARY_BG_COLOR: "#f7f6f2", 29 | DARK_PRIMARY_BG_COLOR: "#111827", 30 | DARK_SECONDARY_BG_COLOR: "#1F2937", 31 | }, 32 | minHeight: { 33 | PAGE_MIN_HEIGHT: "calc(100vh - 12rem)", 34 | }, 35 | keyframes: { 36 | shift: { 37 | from: { transform: "translateX(0)" }, 38 | to: { transform: "translateX(-100%)" }, 39 | }, 40 | }, 41 | animation: { 42 | shift: "shift 15s infinite linear", 43 | }, 44 | }, 45 | }, 46 | plugins: [require("daisyui")], 47 | daisyui: { 48 | themes: false, 49 | }, 50 | variants: { 51 | extend: { 52 | backgroundClip: ["responsive"], 53 | }, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-education-consulting-website", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@babel/runtime": "^7.22.3", 13 | "@emotion/react": "^11.11.0", 14 | "@emotion/styled": "^11.11.0", 15 | "@heroicons/react": "^1.0.6", 16 | "@hookform/resolvers": "^3.1.0", 17 | "@mui/material": "^5.13.0", 18 | "@sendgrid/mail": "^7.7.0", 19 | "@tremor/react": "^2.8.3", 20 | "@types/node": "18.15.11", 21 | "@types/react": "18.0.37", 22 | "@types/react-dom": "18.0.11", 23 | "@vercel/analytics": "^1.0.1", 24 | "autoprefixer": "10.4.14", 25 | "bcrypt": "^5.1.0", 26 | "daisyui": "^2.51.5", 27 | "dynamodb-toolbox": "^0.9.2", 28 | "eslint": "8.38.0", 29 | "eslint-config-next": "^13.4.8", 30 | "gsap": "^3.11.5", 31 | "heroicons": "^2.0.17", 32 | "jsonwebtoken": "^9.0.0", 33 | "next": "^13.4.8", 34 | "next-auth": "^4.22.1", 35 | "next-auth-client": "^1.5.0", 36 | "next-themes": "^0.2.1", 37 | "nookies": "^2.5.2", 38 | "postcss": "8.4.22", 39 | "react": "^18.2.0", 40 | "react-calendly": "^4.1.1", 41 | "react-dom": "^18.2.0", 42 | "react-google-recaptcha-v3": "^1.10.1", 43 | "react-hook-form": "^7.43.9", 44 | "react-icons": "^4.8.0", 45 | "react-query": "^3.39.3", 46 | "sharp": "^0.32.1", 47 | "tailwindcss": "3.3.1", 48 | "typescript": "5.0.4", 49 | "uuid": "^9.0.1", 50 | "zod": "^3.21.4" 51 | }, 52 | "devDependencies": { 53 | "@babel/plugin-transform-runtime": "^7.22.2", 54 | "@babel/preset-env": "^7.22.2", 55 | "@types/bcrypt": "^5.0.0", 56 | "@types/jsonwebtoken": "^9.0.2", 57 | "@types/react-query": "^1.2.9", 58 | "@types/uuid": "^9.0.7", 59 | "prettier": "^3.0.0", 60 | "prettier-plugin-tailwindcss": "^0.3.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/components/AdminLogin.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import PrimaryButton from "../components/PrimaryButton"; 3 | import { useAdminLogin } from "../hooks/useAdminLogin"; 4 | import { useLanguageContext } from "../hooks/useLanguageContext"; 5 | 6 | export default function AdminLogin() { 7 | const { usernameRef, passwordRef, handleAdminLogin } = useAdminLogin(); 8 | const { language } = useLanguageContext(); 9 | 10 | return ( 11 |
12 |
13 |

14 | {language === "en" ? "Admin Login" : "Admin Girişi"} 15 |

16 |
21 | { 26 | usernameRef.current = e.target.value; 27 | }} 28 | /> 29 | { 34 | passwordRef.current = e.target.value; 35 | }} 36 | /> 37 | 38 | 43 | 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /app/hooks/useHeader.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | import { useLanguageContext } from "./useLanguageContext"; 3 | import { gsap } from "gsap"; 4 | 5 | interface NavBarItem { 6 | name: { 7 | en: string; 8 | tr: string; 9 | }; 10 | href: string; 11 | } 12 | 13 | interface IUseHeaderReturn { 14 | language: string; 15 | NAV_BAR_ITEMS: NavBarItem[]; 16 | activeLink: string; 17 | setActiveLink: React.Dispatch>; 18 | headerRef: React.RefObject; 19 | } 20 | 21 | export function useHeader(): IUseHeaderReturn { 22 | const language = useLanguageContext().language; 23 | const NAV_BAR_ITEMS: NavBarItem[] = [ 24 | { 25 | name: { 26 | en: "HOME", 27 | tr: "ANASAYFA", 28 | }, 29 | href: "/", 30 | }, 31 | { 32 | name: { 33 | en: "SERVICES", 34 | tr: "HİZMETLER", 35 | }, 36 | href: "/services", 37 | }, 38 | { 39 | name: { 40 | en: "FAQ", 41 | tr: "SSS", 42 | }, 43 | href: "/faq", 44 | }, 45 | { 46 | name: { 47 | en: "CONTACT", 48 | tr: "İLETİŞİM", 49 | }, 50 | href: "/contact", 51 | }, 52 | ]; 53 | 54 | const [activeLink, setActiveLink] = useState(""); 55 | const headerRef = useRef(null); 56 | 57 | useEffect(() => { 58 | // Set active link(necessary for accessing via url with specific pathnames; e.g. .../about) 59 | if (typeof window !== "undefined") { 60 | setActiveLink(window.location.pathname); 61 | } 62 | 63 | // GSAP Animations 64 | const headerAnimation = gsap.to(headerRef.current!, { 65 | duration: 0.2, 66 | y: 0, 67 | ease: "power3.out", 68 | }); 69 | 70 | const headerAnimation2 = gsap.to(headerRef.current!, { 71 | duration: 0.2, 72 | opacity: 1, 73 | ease: "power3.out", 74 | }); 75 | 76 | const tl = gsap.timeline(); 77 | tl.add(headerAnimation); 78 | tl.add(headerAnimation2); 79 | }, []); 80 | 81 | return { 82 | language, 83 | NAV_BAR_ITEMS, 84 | activeLink, 85 | setActiveLink, 86 | headerRef, 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /app/components/BasicModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Box from "@mui/material/Box"; 3 | import Typography from "@mui/material/Typography"; 4 | import Modal from "@mui/material/Modal"; 5 | import { Dispatch, SetStateAction } from "react"; 6 | 7 | const style = { 8 | position: "absolute", 9 | top: "50%", 10 | left: "50%", 11 | transform: "translate(-50%, -50%)", 12 | width: 350, 13 | bgcolor: "gray", 14 | border: "1px solid #000", 15 | borderRadius: 5, 16 | boxShadow: 24, 17 | p: 4, 18 | textAlign: "center" as "center", 19 | }; 20 | 21 | type Props = { 22 | header: { success: String; failure: String }; 23 | output: { success: String; failure: String }; 24 | onClose: Dispatch>; 25 | open: boolean; 26 | formStatus: number | undefined; 27 | }; 28 | 29 | export default function BasicModal(props: Props) { 30 | const handleClose = () => props.onClose(false); // wrapper to bypass MUIU props type 31 | 32 | return ( 33 | 39 | 40 | 46 | 47 | {props.formStatus === 200 48 | ? props.header.success 49 | : props.header.failure} 50 | 51 | 52 | 53 | {`${ 54 | props.formStatus === 200 55 | ? props.output.success 56 | : props.output.failure 57 | }`} 58 | 59 |
60 | 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /app/components/ServicePlan.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useLanguageContext } from "../hooks/useLanguageContext"; 3 | import { useServicePlan } from "../hooks/useServicePlan"; 4 | 5 | type ServicePlanProps = { 6 | name: { en: string; tr: string }; 7 | price: string; 8 | description: { en: string; tr: string }; 9 | features: { en: string; tr: string }[]; 10 | }; 11 | 12 | export default function ServicePlan({ name, price, description, features }: ServicePlanProps) { 13 | const { language } = useLanguageContext(); 14 | const { hRef, pRef, spanRef, uRef } = useServicePlan(); 15 | 16 | return ( 17 |
22 |

23 | {language === "en" ? name.en : name.tr} 24 |

25 |

26 | {language === "en" ? description.en : description.tr} 27 |

28 |
29 | ${price} 30 |
31 |
    32 | {features.map((feature) => ( 33 |
  • 34 | 35 | 40 | 41 | {language === "en" ? feature.en : feature.tr} 42 |
  • 43 | ))} 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /app/api/create-user/route.tsx: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import bcrypt from "bcrypt"; 3 | import { verifyJwt } from "@/app/lib/jwt"; 4 | import { UserEntity } from "@/app/entities/UserDDB"; 5 | 6 | export async function POST(request: NextRequest) { 7 | if (request.method === "POST") { 8 | try { 9 | const accessToken = request.headers.get("Authorization"); 10 | if (!accessToken || !verifyJwt(accessToken)) { 11 | return new NextResponse( 12 | JSON.stringify({ 13 | error: "Unauthorized Request: Invalid/No access token", 14 | }), 15 | { 16 | status: 401, 17 | headers: { 18 | "Content-Type": "application/json", 19 | }, 20 | } 21 | ); 22 | } 23 | 24 | const body = await request.json(); 25 | const user = { 26 | username: body.username as string, 27 | password: await bcrypt.hash(body.password, 10), 28 | } 29 | console.log("user data " + user) 30 | 31 | const PK = `USER#${user.username}`; 32 | const SK = 'METADATA'; 33 | const isUserExist = (await UserEntity.get({ PK, SK })).Item 34 | 35 | if (isUserExist !== null) { 36 | return new NextResponse( 37 | JSON.stringify({ 38 | error: "User already exists", 39 | }), 40 | { 41 | status: 400, 42 | headers: { 43 | "Content-Type": "application/json", 44 | }, 45 | } 46 | ); 47 | } 48 | 49 | await UserEntity.put({ 50 | username: user.username, 51 | password: user.password, 52 | }); 53 | 54 | const { password, ...userWithoutPass } = user; 55 | return new NextResponse(JSON.stringify(userWithoutPass), { 56 | status: 200, 57 | headers: { 58 | "Content-Type": "application/json", 59 | }, 60 | }); 61 | } catch (error: any) { 62 | console.log(error.message) 63 | return new NextResponse(JSON.stringify({ error: error.message }), { 64 | status: 400, 65 | headers: { 66 | "Content-Type": "application/json", 67 | }, 68 | }); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/lib/zod.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | //ApplicationForm 4 | export const applicationFormSchema = z.object({ 5 | package: z.enum(["Standard Package", "Full Package", "Undecided"]), 6 | programType: z.enum(["Language School", "MA/MS", "PhD"]), 7 | whyUSA: z 8 | .string() 9 | .min(1) 10 | .max(1000) 11 | .refine((value) => value.trim().length > 0), 12 | academicInterests: z 13 | .string() 14 | .min(1) 15 | .max(1000) 16 | .refine((value) => value.trim().length > 0), 17 | fullName: z 18 | .string() 19 | .min(1) 20 | .max(100) 21 | .regex(/^[a-zA-ZğüşöçİĞÜŞÖÇ\s]*$/) 22 | .refine((value) => value.trim().length > 0), 23 | email: z.string().email(), 24 | phone: z 25 | .string() 26 | .min(10) 27 | .regex(/^[0-9\+]+$/), 28 | citizenship: z 29 | .string() 30 | .min(1) 31 | .max(100) 32 | .refine((value) => value.trim().length > 0), 33 | university: z 34 | .string() 35 | .min(1) 36 | .max(100) 37 | .refine((value) => value.trim().length > 0), 38 | major: z 39 | .string() 40 | .min(1) 41 | .max(100) 42 | .refine((value) => value.trim().length > 0), 43 | gpa: z.string().refine((value) => { 44 | const gpa = parseFloat(value); 45 | return gpa >= 1.8 && gpa <= 4.0; 46 | }), 47 | extracurricular: z 48 | .string() 49 | .min(1) 50 | .max(1000) 51 | .refine((value) => value.trim().length > 0), 52 | workExperience: z 53 | .string() 54 | .min(1) 55 | .max(1000) 56 | .refine((value) => value.trim().length > 0), 57 | englishProficiency: z.enum([ 58 | "1", 59 | "2", 60 | "3", 61 | "4", 62 | "5", 63 | "6", 64 | "7", 65 | "8", 66 | "9", 67 | "10", 68 | ]), 69 | toeflIelts: z 70 | .string() 71 | .min(1) 72 | .max(1000) 73 | .refine((value) => value.trim().length > 0), 74 | gre: z.enum(["Yes", "No"]), 75 | }); 76 | 77 | export type ApplicationFormData = z.infer; 78 | 79 | //Contact Form 80 | export const contactFormSchema = z.object({ 81 | fullName: z 82 | .string() 83 | .min(1) 84 | .max(100) 85 | .regex(/^[a-zA-ZğüşöçİĞÜŞÖÇ\s]*$/) 86 | .refine((value) => value.trim().length > 0), 87 | email: z.string().email(), 88 | phone: z 89 | .string() 90 | .min(10) 91 | .regex(/^[0-9\+]+$/), 92 | question: z.string().min(50).max(1000), 93 | }); 94 | 95 | export type ContactFormData = z.infer; 96 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.tsx: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import CredentialsProvider from "next-auth/providers/credentials"; 3 | 4 | const handler = NextAuth({ 5 | // Configure one or more authentication providers 6 | providers: [ 7 | CredentialsProvider({ 8 | // The name to display on the sign in form (e.g. "Sign in with...") 9 | name: "Credentials", 10 | // `credentials` is used to generate a form on the sign in page. 11 | // You can specify which fields should be submitted, by adding keys to the `credentials` object. 12 | // e.g. domain, username, password, 2FA token, etc. 13 | // You can pass any HTML attribute to the tag through the object. 14 | credentials: { 15 | username: { label: "Username", type: "text", placeholder: "jsmith" }, 16 | password: { label: "Password", type: "password" }, 17 | }, 18 | async authorize(credentials) { 19 | // Add logic here to look up the user from the credentials supplied 20 | const res = await fetch(process.env.NEXTAUTH_URL + "/api/login", { 21 | method: "POST", 22 | body: JSON.stringify({ 23 | username: credentials?.username, 24 | password: credentials?.password, 25 | }), 26 | headers: { "Content-Type": "application/json" }, 27 | }); 28 | const user = await res.json(); 29 | // You need to provide your own logic here that takes the credentials 30 | 31 | if (res.ok && user) { 32 | // If you return an object with contents the user will be authenticated 33 | console.log("nextauth user found!"); 34 | console.log(user); 35 | return user; 36 | } else { 37 | // If you return null or false then the credentials will be rejected 38 | console.log("nextauth user not found!"); 39 | return null; 40 | } 41 | }, 42 | }), 43 | ], 44 | 45 | session: { 46 | strategy: "jwt", 47 | }, 48 | 49 | pages: { 50 | signIn: "/admin", 51 | }, 52 | 53 | callbacks: { 54 | async jwt({ token, user }) { 55 | // Add access_token to the token right after signin 56 | return { ...token, ...user }; 57 | }, 58 | 59 | async session({ session, token }) { 60 | // Add property to session, like an access_token from a provider. 61 | session.user = token as any; 62 | return session; 63 | }, 64 | }, 65 | }); 66 | 67 | export { handler as GET, handler as POST }; 68 | -------------------------------------------------------------------------------- /app/components/Hamburger.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useHamburger } from "../hooks/useHamburger"; 3 | 4 | type HamburgerProps = { 5 | navBarItems: { 6 | href: string; 7 | name: { 8 | en: string; 9 | tr: string; 10 | }; 11 | }[]; 12 | setActiveLink: (activeLink: string) => void; 13 | }; 14 | 15 | export default function Hamburger(props: HamburgerProps) { 16 | const { hamburgerRef, isHamburger, setIsHamburger, language } = 17 | useHamburger(); 18 | const genericHamburgerLine = `h-[2px] w-6 my-1 rounded-full bg-black transition ease transform duration-300`; 19 | 20 | return ( 21 |
26 | {isHamburger && ( 27 |
    31 | {props.navBarItems.map((item) => ( 32 | { 37 | props.setActiveLink(item.href); 38 | setIsHamburger(false); 39 | }} 40 | > 41 | {language === "en" ? item.name.en : item.name.tr} 42 | 43 | ))} 44 |
45 | )} 46 | 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/faq/page.tsx: -------------------------------------------------------------------------------- 1 | import QA from "../components/QA"; 2 | import PageIntroduction from "../components/PageIntroduction"; 3 | import PageStyler from "../components/PageStyler"; 4 | import type { Metadata } from "next"; 5 | 6 | export const metadata: Metadata = { 7 | title: "FAQ | SSS", 8 | description: "Frequently Asked Questions | Sıkça Sorulan Sorular", 9 | }; 10 | 11 | export default function FAQ() { 12 | const FAQ_PAGE_INTRO = { 13 | title: { 14 | en: "Lorem Ipsum Dolor Sit Amet", 15 | tr: "Lorem Ipsum Dolor Sit Amet (TR)", 16 | }, 17 | description: { 18 | en: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 19 | tr: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. (TR)", 20 | }, 21 | }; 22 | 23 | const FAQ_DATA = [ 24 | { 25 | question: { 26 | en: "Lorem ipsum dolor sit amet?", 27 | tr: "Lorem ipsum dolor sit amet? (TR)", 28 | }, 29 | answer: { 30 | en: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor.", 31 | tr: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor. (TR)", 32 | }, 33 | }, 34 | { 35 | question: { 36 | en: "Sed ut perspiciatis unde omnis iste natus error sit?", 37 | tr: "Sed ut perspiciatis unde omnis iste natus error sit? (TR)", 38 | }, 39 | answer: { 40 | en: "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.", 41 | tr: "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium. (TR)", 42 | }, 43 | }, 44 | { 45 | question: { 46 | en: "Ut enim ad minima veniam, quis nostrum exercitationem?", 47 | tr: "Ut enim ad minima veniam, quis nostrum exercitationem? (TR)", 48 | }, 49 | answer: { 50 | en: `Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam. Contact us via email: lorem@ipsum`, 51 | tr: `Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam. Bize şu adresten ulaşabilirsiniz: lorem@ipsum`, 52 | }, 53 | }, 54 | ]; 55 | 56 | return ( 57 | 58 |
59 | 60 |
61 | {FAQ_DATA.map((faq, index) => ( 62 | 63 | ))} 64 |
65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /app/hooks/useLanguageDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { setCookie } from "nookies"; 2 | import { useLanguageContext } from "../hooks/useLanguageContext"; 3 | import { 4 | Dispatch, 5 | MouseEvent, 6 | SetStateAction, 7 | useEffect, 8 | useRef, 9 | useState, 10 | } from "react"; 11 | 12 | interface Language { 13 | name: string; 14 | code: string; 15 | } 16 | 17 | interface UseLanguageDropdownReturn { 18 | context: ReturnType; 19 | LANGUAGES: Language[]; 20 | isDropDownOpen: boolean; 21 | setIsDropDownOpen: Dispatch>; 22 | handleLanguageSelection: (event: MouseEvent) => void; 23 | handleOptionClick: (event: MouseEvent) => void; 24 | dropdownRef: React.RefObject; 25 | } 26 | 27 | export function useLanguageDropdown(): UseLanguageDropdownReturn { 28 | const context = useLanguageContext(); 29 | const [isDropDownOpen, setIsDropDownOpen] = useState(false); 30 | const dropdownRef = useRef(null); 31 | const LANGUAGES: Language[] = [ 32 | { 33 | name: context.language === "en" ? "ENGLISH" : "İNGİLİZCE", 34 | code: "en", 35 | }, 36 | 37 | { 38 | name: context.language === "en" ? "TURKISH" : "TÜRKÇE", 39 | code: "tr", 40 | }, 41 | ]; 42 | 43 | useEffect(() => { 44 | // Update the language value in the cookie 45 | setCookie(null, "language", context.language, { 46 | maxAge: 30 * 24 * 60 * 60, 47 | path: "/", 48 | }); 49 | }, [context.language]); 50 | 51 | useEffect(() => { 52 | const handleClickOutside = (event: any) => { 53 | if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { 54 | setIsDropDownOpen(false); 55 | } 56 | }; 57 | 58 | document.addEventListener("mousedown", handleClickOutside); 59 | 60 | return () => { 61 | document.removeEventListener("mousedown", handleClickOutside); 62 | }; 63 | }, [setIsDropDownOpen]); 64 | 65 | const handleLanguageSelection = ( 66 | event: React.MouseEvent 67 | ): void => { 68 | const value = event.currentTarget.innerText.toLowerCase(); 69 | const language = LANGUAGES.find( 70 | (language) => language.name.toLowerCase() === value 71 | ); 72 | if (language) { 73 | context.setLanguage(language.code); 74 | } 75 | }; 76 | 77 | const handleOptionClick = (event: React.MouseEvent): void => { 78 | handleLanguageSelection(event); 79 | setIsDropDownOpen(false); 80 | }; 81 | 82 | return { 83 | context, 84 | LANGUAGES, 85 | isDropDownOpen, 86 | setIsDropDownOpen, 87 | handleLanguageSelection, 88 | handleOptionClick, 89 | dropdownRef, 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /app/about/page.tsx: -------------------------------------------------------------------------------- 1 | import Cofounder from "../components/Cofounder"; 2 | import PageIntroduction from "../components/PageIntroduction"; 3 | import PageStyler from "../components/PageStyler"; 4 | import type { Metadata } from "next"; 5 | 6 | export const metadata: Metadata = { 7 | title: "About | Hakkımızda", 8 | description: "About us | Hakkımızda", 9 | }; 10 | 11 | export default function About() { 12 | const COFOUNDERS = [ 13 | { 14 | name: "Lorem Ipsum", 15 | title: { 16 | en: "Co-founder", 17 | tr: "Kurucu", 18 | }, 19 | description: { 20 | en: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor.", 21 | tr: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor.", 22 | }, 23 | imagePath: "/placeholder1.webp", 24 | imageAlt: "Placeholder image of a co-founder", 25 | }, 26 | { 27 | name: "Dolor Sit", 28 | title: "Co-founder", 29 | description: { 30 | en: "Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi.", 31 | tr: "Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi.", 32 | }, 33 | imagePath: "/placeholder2.webp", 34 | imageAlt: "Placeholder image of a co-founder", 35 | }, 36 | ]; 37 | 38 | const ABOUT_PAGE_INTRODUCTION = { 39 | title: { 40 | en: "Our Team", 41 | tr: "Ekibimiz", 42 | }, 43 | description: { 44 | en: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam.", 45 | tr: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam.", 46 | }, 47 | }; 48 | 49 | return ( 50 | 51 |
52 | 53 |
54 | {COFOUNDERS.map((cofounder, index) => ( 55 | 56 | ))} 57 |
58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /app/hooks/useHeliosForms.tsx: -------------------------------------------------------------------------------- 1 | import { zodResolver } from "@hookform/resolvers/zod"; 2 | import { useState, useRef, useLayoutEffect } from "react"; 3 | import { useGoogleReCaptcha } from "react-google-recaptcha-v3"; 4 | import { useForm } from "react-hook-form"; 5 | import { useMutation } from "react-query"; 6 | import { 7 | ApplicationFormData, 8 | ContactFormData, 9 | applicationFormSchema, 10 | contactFormSchema, 11 | } from "../lib/zod"; 12 | import { gsap } from "gsap"; 13 | import { useLanguageContext } from "./useLanguageContext"; 14 | 15 | export function useHeliosForms(formType: "application" | "contact") { 16 | const [isModalOpen, setIsModalOpen] = useState(false); 17 | const language = useLanguageContext().language; 18 | const formRef = useRef(null); 19 | 20 | const customFormSchema = 21 | formType === "application" ? applicationFormSchema : contactFormSchema; 22 | 23 | const { 24 | register, 25 | handleSubmit, 26 | reset, 27 | formState: { errors }, 28 | } = useForm({ 29 | resolver: zodResolver(customFormSchema), 30 | mode: "onTouched", 31 | }); 32 | const { executeRecaptcha } = useGoogleReCaptcha(); 33 | 34 | // Get the reCAPTCHA token by executing it with an action name 35 | 36 | const onSubmit = async (formData: ApplicationFormData | ContactFormData) => { 37 | if (!executeRecaptcha) { 38 | console.log("executeRecaptcha not yet available"); 39 | throw new Error("executeRecaptcha not yet available"); 40 | } 41 | const token = await executeRecaptcha("submit"); 42 | const response = await fetch("/api/submit-form", { 43 | method: "POST", 44 | headers: { 45 | "Content-Type": "application/json", 46 | }, 47 | body: JSON.stringify({ formData, token, formType }), 48 | }); 49 | 50 | return response; 51 | }; 52 | 53 | async function handleMutationSubmit( 54 | formData: ApplicationFormData | ContactFormData 55 | ) { 56 | await formSubmissionMutation.mutateAsync(formData); 57 | } 58 | 59 | const formSubmissionMutation = useMutation(onSubmit, { 60 | onSuccess: (response) => { 61 | const data = response.json(); //for future debugging 62 | setIsModalOpen(true); 63 | if (response.status === 200) { 64 | reset(); 65 | } 66 | }, 67 | onError: (error) => { 68 | console.log(error); 69 | throw new Error(error as string); 70 | }, 71 | }); 72 | 73 | useLayoutEffect(() => { 74 | gsap.fromTo( 75 | formRef.current, 76 | { opacity: 0, y: 100 }, 77 | { opacity: 1, y: 0, duration: 0.5 } 78 | ); 79 | }, []); 80 | 81 | return { 82 | register, 83 | handleMutationSubmit, 84 | handleSubmit, 85 | errors, 86 | isModalOpen, 87 | setIsModalOpen, 88 | language, 89 | formSubmissionMutation, 90 | formRef, 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./globals.css"; 3 | import Header from "./components/Header"; 4 | import { LanguageProvider } from "./contexts/LanguageProvider"; 5 | import ThemeProviders from "./contexts/ThemeProvider"; 6 | import Footer from "./components/Footer"; 7 | import { Nunito_Sans } from "next/font/google"; 8 | import QueryProvider from "./contexts/QueryProvider"; 9 | import AuthSessionProvider from "./contexts/AuthSessionProvider"; 10 | import ThemeChanger from "./components/ThemeChanger"; 11 | import { Analytics } from "@vercel/analytics/react"; 12 | import CalendlyWidget from "./components/CalendlyWidget"; 13 | import MaintenanceView from "./components/MaintenanceView"; 14 | 15 | export const metadata = { 16 | title: 17 | "Helios Admissions - Amerika'da Yüksek lisans, Doktora ve Dil Okulu Danışmanlığı | Helios Admissions - Graduate School, PhD and Language School Consulting in the United States ", 18 | description: "ABD merkezli Helios Admissions Resmi Web Sitesi | Official website of Helios Education Consulting Company based in the United States", 19 | icons: [ 20 | { 21 | rel: "icon", 22 | url: "/favicon/apple-touch-icon.png", 23 | sizes: "180x180", 24 | type: "image/png", 25 | }, 26 | { 27 | rel: "icon", 28 | url: "/favicon/favicon-32x32.png", 29 | sizes: "32x32", 30 | type: "image/png", 31 | }, 32 | { 33 | rel: "icon", 34 | url: "/favicon/favicon-16x16.png", 35 | sizes: "16x16", 36 | type: "image/png", 37 | }, 38 | { 39 | rel: "manifest", 40 | url: "/favicon/site.webmanifest", 41 | }, 42 | ], 43 | }; 44 | 45 | const nunito_sans = Nunito_Sans({ 46 | weight: ["300", "400", "700"], 47 | style: ["normal", "italic"], 48 | subsets: ["latin-ext", "latin"], 49 | display: "optional", 50 | }); 51 | 52 | interface RootLayoutProps { 53 | children: React.ReactNode; 54 | session: any; 55 | } 56 | 57 | export default function RootLayout({ children }: RootLayoutProps) { 58 | const isMaintenanceMode = process.env.NEXT_PUBLIC_MAINTENANCE_MODE === "true"; 59 | 60 | return ( 61 | 62 | 63 | {isMaintenanceMode ? ( 64 | 65 | ) : ( 66 | 67 | 68 | 69 | 70 |
71 | 72 | 73 | {children} 74 | 75 |