87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/Rates/Rates.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Container from "../Container";
3 | import { Label } from "../ui/label";
4 | import RatesCalculator from "./RatesCalculator";
5 |
6 | const Rates = () => {
7 | return (
8 |
9 |
10 |
11 |
12 | {/* Left */}
13 |
14 |
Today's Rates *
15 |
16 |
17 |
18 |
19 |
20 | Term
21 |
22 |
23 | Rate
24 |
25 |
26 | APR
27 |
28 |
29 |
30 |
31 |
32 | 30 Yr Fxd
33 | 5.747%
34 | 5.819%
35 |
36 |
37 | 30 Yr Fxd
38 | 5.747%
39 | 5.819%
40 |
41 |
42 | 30 Yr Fxd
43 | 5.747%
44 | 5.819%
45 |
46 |
47 |
48 |
49 |
50 | *=This is only an estimate, provided for illustrative purposes
51 | only. Actual rates and payments may vary. It does not constitute a
52 | quote.
53 |
54 |
55 |
56 | {/* Right */}
57 |
58 |
65 |
66 |
67 |
68 |
69 |
70 |
77 |
78 | {/* Calculator In Today's Rates Section */}
79 |
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default Rates;
89 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardCards.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2 | import {
3 | Users,
4 | ShoppingCart,
5 | DollarSign,
6 | BarChart2,
7 | Settings,
8 | Home,
9 | FileText,
10 | Bell,
11 | LogOut,
12 | SquarePen,
13 | } from "lucide-react";
14 | import Container from "../Container";
15 |
16 | const DashboardCards = () => {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 | Total Users
25 |
26 |
27 |
28 |
29 | 1,234
30 |
31 | +10% from last month
32 |
33 |
34 |
35 |
36 |
37 |
38 | Total Orders
39 |
40 |
41 |
42 |
43 | 567
44 |
45 | +5% from last week
46 |
47 |
48 |
49 |
50 |
51 | Revenue
52 |
53 |
54 |
55 | $12,345
56 |
57 | +15% from last month
58 |
59 |
60 |
61 |
62 |
63 |
64 | Analytics
65 |
66 |
67 |
68 |
69 | 87%
70 |
71 | Website traffic increase
72 |
73 |
74 |
75 |
76 |
77 |
78 | Active Tasks
79 |
80 |
81 |
82 |
83 | 13
84 |
85 | 5 tasks overdue
86 |
87 |
88 |
89 |
90 |
91 |
92 | );
93 | }
94 |
95 | export default DashboardCards;
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/src/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/src/components/Rates/RatesCalculator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState } from "react";
3 | import { Button } from "../ui/button";
4 | import { Input } from "../ui/input";
5 | import { Label } from "../ui/label";
6 | import { CiCalculator2 } from "react-icons/ci";
7 | import { sleep } from "@/lib/sleep";
8 | import { Slider } from "../ui/slider";
9 |
10 | const RatesCalculator = () => {
11 | // States
12 | const [loanAmount, setLoanAmount] = useState(100000);
13 | const [interestRate, setInterestRate] = useState(4.02);
14 | const [loanDuration, setLoanDuration] = useState(25);
15 | // State for storing the calculated EMI
16 | const [emi, setEmi] = useState(null);
17 |
18 | // Calculating Loading
19 | const [calculating, setCalculating] = useState(false);
20 |
21 | // Submit Handler on Calculate Button
22 | const submitHandler = async (e: React.FormEvent) => {
23 | e.preventDefault();
24 | setCalculating(true);
25 | await sleep(2000);
26 | calculateEMI();
27 | setCalculating(false);
28 | };
29 |
30 | // calculateEMI Function
31 | const calculateEMI = () => {
32 | const P = loanAmount; // Principal amount
33 | const r = interestRate / 12 / 100; // Monthly interest rate
34 | const n = loanDuration * 12; // Loan duration in months
35 |
36 | if (P && r && n) {
37 | const emi = (P * r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);
38 | setEmi(emi);
39 | } else {
40 | alert("Please fill all fields with valid values");
41 | }
42 | };
43 |
44 |
45 | return (
46 |
47 |
Calculate EMI Now!
48 |
140 |
141 | );
142 | };
143 |
144 | export default RatesCalculator;
145 |
--------------------------------------------------------------------------------
/src/components/Dashboard/NewPost2.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import { useForm } from "react-hook-form";
5 | import { z } from "zod";
6 | import { useFormStatus } from "react-dom";
7 |
8 | import { toast } from "@/hooks/use-toast";
9 | import { Button } from "../ui/button";
10 | import {
11 | Form,
12 | FormControl,
13 | FormField,
14 | FormItem,
15 | FormLabel,
16 | FormMessage,
17 | } from "@/components/ui/form";
18 | import { Input } from "@/components/ui/input";
19 | import { Checkbox } from "@/components/ui/checkbox";
20 | import { Label } from "@/components/ui/label";
21 | import { Textarea } from "../ui/textarea";
22 | import { ToastAction } from "../ui/toast";
23 | import { addPost } from "@/lib/action";
24 | import SubmitButton from "./SubmitButton";
25 | import { useState } from "react";
26 |
27 | export const FormSchema = z.object({
28 | title: z.string().min(2, {
29 | message: "Title must be at least 2 characters.",
30 | }),
31 | content: z.string().min(2, {
32 | message: "Content must be at least 2 characters.",
33 | }),
34 | imageUrl: z.string().min(2, {
35 | message: "Image URL must be at least 2 characters.",
36 | }),
37 | });
38 |
39 | export function NewPost2() {
40 |
41 | const [isSubmitting, setIsSubmitting] = useState(false);
42 |
43 | const categories = ["Technology", "Travel", "Food", "Lifestyle", "Fashion"];
44 |
45 | const form = useForm>({
46 | resolver: zodResolver(FormSchema),
47 | defaultValues: {
48 | title: "",
49 | },
50 | });
51 |
52 | const { reset } = form;
53 |
54 | const sleep = (ms:number)=> new Promise(r=> setTimeout(r, ms));
55 |
56 | async function onSubmit(data: z.infer) {
57 | setIsSubmitting(true)
58 | await addPost(data);
59 |
60 | await sleep(3000)
61 |
62 | // Toast
63 | toast({
64 | title: "Done! ",
65 | description: "Misson completed succesfully",
66 | className: "bg-white",
67 | action: 👍 ,
68 | });
69 |
70 | reset({title:"", content:"", imageUrl:""})
71 | setIsSubmitting(false)
72 | }
73 |
74 | return (
75 |
76 |
Create a new post
77 |
154 |
155 |
156 | );
157 | }
158 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Cross2Icon } from "@radix-ui/react-icons"
5 | import * as ToastPrimitives from "@radix-ui/react-toast"
6 | import { cva, type VariantProps } from "class-variance-authority"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronDownIcon } from "@radix-ui/react-icons"
3 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
4 | import { cva } from "class-variance-authority"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const NavigationMenu = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
20 | {children}
21 |
22 |
23 | ))
24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25 |
26 | const NavigationMenuList = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40 |
41 | const NavigationMenuItem = NavigationMenuPrimitive.Item
42 |
43 | const navigationMenuTriggerStyle = cva(
44 | "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-lg font-medium transition-colors duration-300 hover:text-[#e75b0f] focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
45 | )
46 |
47 | const NavigationMenuTrigger = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, children, ...props }, ref) => (
51 |
56 | {children}{" "}
57 |
61 |
62 | ))
63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64 |
65 | const NavigationMenuContent = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
77 | ))
78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79 |
80 | const NavigationMenuLink = NavigationMenuPrimitive.Link
81 |
82 | const NavigationMenuViewport = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 |
95 |
96 | ))
97 | NavigationMenuViewport.displayName =
98 | NavigationMenuPrimitive.Viewport.displayName
99 |
100 | const NavigationMenuIndicator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
112 |
113 |
114 | ))
115 | NavigationMenuIndicator.displayName =
116 | NavigationMenuPrimitive.Indicator.displayName
117 |
118 | export {
119 | navigationMenuTriggerStyle,
120 | NavigationMenu,
121 | NavigationMenuList,
122 | NavigationMenuItem,
123 | NavigationMenuContent,
124 | NavigationMenuTrigger,
125 | NavigationMenuLink,
126 | NavigationMenuIndicator,
127 | NavigationMenuViewport,
128 | }
129 |
--------------------------------------------------------------------------------
/src/components/NavbarLinks.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import Link from "next/link";
5 |
6 | import { cn } from "@/lib/utils";
7 | import { FcCalculator, FcNews } from "react-icons/fc";
8 | import {
9 | NavigationMenu,
10 | NavigationMenuContent,
11 | NavigationMenuIndicator,
12 | NavigationMenuItem,
13 | NavigationMenuLink,
14 | NavigationMenuList,
15 | NavigationMenuTrigger,
16 | } from "@/components/ui/navigation-menu";
17 | import { navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
18 | import Image from "next/image";
19 |
20 | const components: { title: string; href: string; description: string }[] = [
21 | {
22 | title: "Application Form 1",
23 | href: "/apply2",
24 | description:
25 | "This Application Form uses Mongoose and MongoDB.",
26 | },
27 | {
28 | title: "Application Form 2",
29 | href: "/apply4",
30 | description:
31 | "This Application Form uses Prisma and MongoDB.",
32 | }
33 | ];
34 |
35 | export function NavbarLinks() {
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 | Calculators
44 |
45 |
46 |
47 |
48 |
49 |
50 | {/*
*/}
51 |
52 |
62 |
63 |
64 |
65 |
66 |
67 | Calculate your monthly mortgage payments and view details.
68 |
69 |
73 | Calculate how much home you can afford based on your financial
74 | situation.
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | Apply Forms
83 |
84 |
85 |
86 |
87 | {components.map((component) => (
88 |
93 | {component.description}
94 |
95 | ))}
96 |
97 |
98 |
99 |
100 |
101 |
102 | Blog
103 |
104 |
105 |
106 |
107 |
108 |
109 | Contact
110 |
111 |
112 |
113 |
114 |
115 |
116 | );
117 | }
118 |
119 | const ListItem = React.forwardRef<
120 | React.ElementRef<"a">,
121 | React.ComponentPropsWithoutRef<"a">
122 | >(({ className, title, children, ...props }, ref) => {
123 | return (
124 |
125 |
126 |
134 |
135 | {title}
136 |
137 |
138 | {children}
139 |
140 |
141 |
142 |
143 | );
144 | });
145 | ListItem.displayName = "ListItem";
146 |
--------------------------------------------------------------------------------
/src/app/contact/page.tsx:
--------------------------------------------------------------------------------
1 | import Container from "@/components/Container";
2 | import { Button } from "@/components/ui/button";
3 |
4 | const page = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | If you're new to buy to let or already with us, don’t hesitate
14 | to contact us. There are dedicated teams ready to help.
15 |
16 |
17 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Name
34 |
35 |
41 |
42 |
43 |
68 |
69 |
118 |
119 |
120 |
121 | Message
122 |
123 |
124 |
129 |
130 |
131 |
132 |
136 | Send
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | );
147 | };
148 |
149 | export default page;
150 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Dashboard2.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Input } from "@/components/ui/input";
5 | import {
6 | Home,
7 | SquarePen,
8 | CircleCheckBig,
9 | Sparkles,
10 | Rss,
11 | Power,
12 | Headset,
13 | Search,
14 | } from "lucide-react";
15 | import DashboardCards from "./DashboardCards";
16 | import { useState } from "react";
17 | import { NewPost2 } from "./NewPost2";
18 | import Link from "next/link";
19 | import { SupportModal } from "../SupportModal/SupportModal";
20 | import { useClerk } from "@clerk/nextjs";
21 | import NewPostPrisma from "./NewPostPrisma";
22 |
23 | export default function Dashboard2() {
24 |
25 | const [userSelect, setUserSelect] = useState( );
26 | const { signOut } = useClerk();
27 |
28 | return (
29 |
30 | {/* Sidebar */}
31 |
32 |
33 |
Admin Panel
34 |
35 |
36 | setUserSelect( )}
40 | >
41 |
42 | Dashboard
43 |
44 |
45 | setUserSelect( )}
47 | variant="ghost"
48 | className="w-full justify-start px-4 py-5 text-left text-lg"
49 | >
50 |
51 | New Post - (Mongoose)
52 |
53 | setUserSelect( )}
55 | variant="ghost"
56 | className="w-full justify-start px-4 py-5 text-left text-lg"
57 | >
58 |
59 | New Post - (Prisma)
60 |
61 |
62 |
66 |
67 | Blog - (Mongoose)
68 |
69 |
70 |
71 |
75 |
76 | Blog - (Prisma)
77 |
78 |
79 |
80 |
84 |
85 | Apply now
86 |
87 |
88 |
89 |
93 |
94 | Get Start
95 |
96 |
97 |
98 |
99 |
103 |
104 | Support
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | signOut({ redirectUrl: '/' })}
116 | >
117 |
118 | Log out
119 |
120 | {/*
124 |
125 | Notifications
126 |
127 |
131 |
132 | Settings
133 | */}
134 |
135 |
136 |
137 | {/* Main Content */}
138 |
139 | {/* Header */}
140 |
158 |
159 | {/* Dashboard Content */}
160 |
{userSelect}
161 |
162 |
163 |
164 |
165 |
166 | );
167 | }
168 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import {
5 | CaretSortIcon,
6 | CheckIcon,
7 | ChevronDownIcon,
8 | ChevronUpIcon,
9 | } from "@radix-ui/react-icons"
10 | import * as SelectPrimitive from "@radix-ui/react-select"
11 |
12 | import { cn } from "@/lib/utils"
13 |
14 | const Select = SelectPrimitive.Root
15 |
16 | const SelectGroup = SelectPrimitive.Group
17 |
18 | const SelectValue = SelectPrimitive.Value
19 |
20 | const SelectTrigger = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, children, ...props }, ref) => (
24 | span]:line-clamp-1",
28 | className
29 | )}
30 | {...props}
31 | >
32 | {children}
33 |
34 |
35 |
36 |
37 | ))
38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
39 |
40 | const SelectScrollUpButton = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 |
53 |
54 | ))
55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
56 |
57 | const SelectScrollDownButton = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 |
70 |
71 | ))
72 | SelectScrollDownButton.displayName =
73 | SelectPrimitive.ScrollDownButton.displayName
74 |
75 | const SelectContent = React.forwardRef<
76 | React.ElementRef,
77 | React.ComponentPropsWithoutRef
78 | >(({ className, children, position = "popper", ...props }, ref) => (
79 |
80 |
91 |
92 |
99 | {children}
100 |
101 |
102 |
103 |
104 | ))
105 | SelectContent.displayName = SelectPrimitive.Content.displayName
106 |
107 | const SelectLabel = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ className, ...props }, ref) => (
111 |
116 | ))
117 | SelectLabel.displayName = SelectPrimitive.Label.displayName
118 |
119 | const SelectItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | SelectItem.displayName = SelectPrimitive.Item.displayName
140 |
141 | const SelectSeparator = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef
144 | >(({ className, ...props }, ref) => (
145 |
150 | ))
151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
152 |
153 | export {
154 | Select,
155 | SelectGroup,
156 | SelectValue,
157 | SelectTrigger,
158 | SelectContent,
159 | SelectLabel,
160 | SelectItem,
161 | SelectSeparator,
162 | SelectScrollUpButton,
163 | SelectScrollDownButton,
164 | }
165 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Full Stack Mortgage Website
4 |
5 |
6 | A FullStack web application suitable for Mortgage use.
7 | In this project, an admin dashboard has also been developed, meaning you can publish new content.
8 | Authentication in this project is handled using Clerk.
9 |
10 | For communication with the MongoDB server, I used the mongoose library.
11 | Additionally, the posts you publish are available at the /blog address, where the information is updated using revalidationPath after your post is registered.
12 | I used Shadcn components.
13 |
14 | Dark/Light mode has been developed in this project.
15 | On the /apply page, there is a multi-step form developed using Surveyjs.io. This was my first time using Surveyjs in this project, and it was amazing! I highly recommend you try it 🤗.
16 | It's easy to use. If you need guidance, feel free to check out my post on LinkedIn:
17 |
18 | https://www.linkedin.com/feed/update/urn:li:activity:7240008028232572929/
19 |
20 | Thank you for your attention.
21 | This project is available at the following address:
22 | [**Demo** ✔️](https://mortgages-hamed.vercel.app/).\
23 | Thank you sincerely. 🙏
24 |
25 | On this admin dashboard page, I've developed two forms—one that submits data using Mongoose and another using Prisma. The form data is validated with React Hook Form and Zod.
26 | https://mortgages-hamed.vercel.app/admin
27 |
28 | On this page, the information is fetched and displayed using Prisma.
29 | https://mortgages-hamed.vercel.app/blogprisma
30 |
31 | On this page, the information is fetched and displayed using Mongoose.
32 | https://mortgages-hamed.vercel.app/blog
33 |
34 | On the homepage, I've added a minimal Calculate EMI feature.
35 | https://mortgages-hamed.vercel.app/
36 |
37 | This form is registered in the MongoDB database using Prisma.
38 | https://mortgages-hamed.vercel.app/apply4
39 |
40 | This form is registered using Mongoose.
41 | https://mortgages-hamed.vercel.app/apply2
42 |
43 | This is an advanced Mortgage Affordability Calculator that can calculate very quickly.
44 | https://mortgages-hamed.vercel.app/affordability
45 |
46 | In this Mortgage EMI Calculator, I've tried to compute user inputs in real-time and display the results along with a chart.
47 | https://mortgages-hamed.vercel.app/calculator
48 |
49 |
50 |
51 |
57 |
58 |
59 |
65 |
66 |
67 |
72 |
73 |
78 |
79 |
80 |
85 |
86 |
87 |
92 |
93 |
94 |
99 |
100 |
101 |
106 |
107 |
108 |
113 |
114 |
115 |
120 |
121 |
122 |
123 |
124 | 
125 | 
126 | 
127 | 
128 | 
129 | 
130 | 
131 | 
132 | 
133 | 
134 |
135 | Full:
136 | 
137 |
138 |
139 | Connect with me:
140 |
141 |
142 |
150 |
152 |
162 |
172 |
173 |
174 |
175 |
176 |
177 |
--------------------------------------------------------------------------------
/src/lib/action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import Post from "@/models/PostModel";
4 | import dbConnect from "./connectToDb";
5 | import { revalidatePath } from "next/cache";
6 | import { z } from "zod";
7 | import Apply from "@/models/ApplyModel";
8 | import prisma from "./db";
9 |
10 | // export interface FormDataType {
11 | // title: string;
12 | // content: string;
13 | // imageUrl: string;
14 | // }
15 |
16 | // POST Method
17 | export const addPost = async (formData: any) => {
18 | try {
19 | await dbConnect();
20 | const data = {
21 | title: formData.title,
22 | content: formData.content,
23 | imageUrl: formData.imageUrl,
24 | };
25 | const saveUser = await new Post(data).save();
26 | revalidatePath("/blog");
27 | console.log(saveUser);
28 | } catch (error) {
29 | console.log(error);
30 | }
31 | };
32 |
33 | // GET Method
34 | export const getPosts = async () => {
35 | try {
36 | await dbConnect();
37 | const response = await Post.find().exec();
38 | console.log(response);
39 | return response;
40 | } catch (error) {
41 | console.log(error);
42 | }
43 | };
44 |
45 | // POST APPLY 2
46 | // zod Schema Validation
47 | const applyFormSchema = z.object({
48 | fullName: z.string(),
49 | email: z.string().email(),
50 | phoneNumber: z
51 | .string()
52 | .min(10)
53 | .refine((val) => /^\d+$/.test(val), {
54 | message: "Phone number must contain only digits",
55 | }),
56 | loanAmount: z.number().min(1000).max(1000000),
57 | loanPurpose: z.string(),
58 | employmentStatus: z.string(),
59 | annualIncome: z.number(),
60 | creditScore: z.number().min(300).max(800),
61 | hasCollateral: z.boolean(),
62 | agreeToTerms: z.boolean(),
63 | });
64 |
65 | export const submitApply = async (data: z.infer) => {
66 | console.log("pre try");
67 | try {
68 | await dbConnect();
69 | console.log("Connected to database");
70 |
71 | const validatedData = applyFormSchema.parse(data);
72 |
73 | // const applicationData = {
74 | // ...validatedData
75 | // }
76 | const saveForm = await new Apply(validatedData).save();
77 | // revalidatePath('/blog')
78 | console.log("Application saved:", saveForm);
79 | return { success: true, message: "Application submitted successfully." };
80 | } catch (error) {
81 | console.error("Error submitting loan application:", error);
82 | return {
83 | success: false,
84 | message:
85 | "There was an error submitting your application. Please try again.",
86 | };
87 | }
88 | };
89 |
90 | // POST APPLY 3
91 | const applicationSchema = z.object({
92 | personalInfo: z.object({
93 | firstName: z.string(),
94 | lastName: z.string(),
95 | email: z.string().email(),
96 | phone: z.string(),
97 | dob: z.string(),
98 | }),
99 | loanInfo: z.object({
100 | loanType: z.enum(["mortgage", "personal", "auto"]),
101 | loanAmount: z.number(),
102 | loanTerm: z.number(),
103 | interestRate: z.number(),
104 | }),
105 | employmentInfo: z.object({
106 | employmentStatus: z.enum(["employed", "self-employed", "unemployed"]),
107 | employerName: z.string().optional(),
108 | annualIncome: z.number(),
109 | yearsAtJob: z.number(),
110 | }),
111 | assets: z.array(
112 | z.object({
113 | assetType: z.enum(["savings", "investments", "property"]),
114 | value: z.number(),
115 | })
116 | ),
117 | liabilities: z.array(
118 | z.object({
119 | liabilityType: z.enum(["credit_card", "student_loan", "car_loan"]),
120 | amount: z.number(),
121 | })
122 | ),
123 | documents: z.object({
124 | idProof: z.any().optional(),
125 | incomeProof: z.any().optional(),
126 | bankStatements: z.any().optional(),
127 | }),
128 | termsAccepted: z.boolean(),
129 | });
130 |
131 | export async function submitApply3(data: z.infer) {
132 | try {
133 | // Validate the data
134 | const validatedData = applicationSchema.parse(data);
135 |
136 | // In a real application, you would process the file uploads here
137 | // For this example, we'll just remove the file objects
138 | const applicationData = {
139 | ...validatedData,
140 | documents: {
141 | idProof: validatedData.documents.idProof ? "Uploaded" : "Not provided",
142 | incomeProof: validatedData.documents.incomeProof
143 | ? "Uploaded"
144 | : "Not provided",
145 | bankStatements: validatedData.documents.bankStatements
146 | ? "Uploaded"
147 | : "Not provided",
148 | },
149 | };
150 |
151 | // Save the application to the database
152 | const saveForm3 = await new Apply(validatedData).save();
153 | // revalidatePath('/blog')
154 | console.log("Application saved:", saveForm3);
155 | return { success: true, message: "Application submitted successfully." };
156 |
157 | // In a real application, you might want to send an email to the applicant here
158 |
159 | return {
160 | success: true,
161 | message: "Your loan application has been submitted successfully!",
162 | };
163 | } catch (error) {
164 | console.error("Error submitting loan application:", error);
165 | return {
166 | success: false,
167 | message:
168 | "There was an error submitting your application. Please try again.",
169 | };
170 | }
171 | }
172 |
173 | // Create post with Prisma action and server side validation
174 | const postPrismaSchema = z.object({
175 | title: z.string().min(1).max(100),
176 | description: z.string().min(1).max(500),
177 | imageUrl: z.string().url().optional().or(z.literal("")),
178 | });
179 |
180 | export async function createPostPrisma(
181 | data: z.infer
182 | ) {
183 | try{
184 | const validatedData = postPrismaSchema.parse(data);
185 |
186 | const post = await prisma.postprisma.create({
187 | data: {
188 | title: validatedData.title,
189 | description: validatedData.description,
190 | imageUrl: validatedData.imageUrl || null
191 | }
192 | })
193 | console.log(post)
194 | return {success: true, post}
195 | }catch(error){
196 | if(error instanceof z.ZodError){
197 | return { success: false, message: 'Validation error', errors: error.errors }
198 | }
199 | console.error('Failed to create post:', error)
200 | return { success: false, message: 'Failed to create post' }
201 | }
202 | }
203 |
204 | // Get post with Prisma action
205 | export async function getPostsPrisma() {
206 | try {
207 | const posts = await prisma.postprisma.findMany();
208 | return posts;
209 | } catch (error) {
210 | console.error("Error fetching posts:", error);
211 | throw new Error("Failed to fetch posts.");
212 | }
213 | }
214 |
215 |
--------------------------------------------------------------------------------
/src/app/start/theme.ts:
--------------------------------------------------------------------------------
1 | export const themeJson = {
2 | "backgroundImage": "https://api.surveyjs.io/private/Surveys/files?name=fff9c109-c767-467f-b0f7-f8f1e4f5b53a",
3 | "backgroundImageFit": "cover",
4 | "backgroundImageAttachment": "fixed",
5 | "backgroundOpacity": 1,
6 | "isPanelless": true,
7 | "cssVariables": {
8 | "--sjs-general-backcolor": "rgba(255, 255, 255, 1)",
9 | "--sjs-general-backcolor-dark": "rgba(248, 248, 248, 1)",
10 | "--sjs-general-backcolor-dim": "rgba(255, 255, 255, 1)",
11 | "--sjs-general-backcolor-dim-light": "rgba(249, 249, 249, 0)",
12 | "--sjs-general-backcolor-dim-dark": "rgba(243, 243, 243, 1)",
13 | "--sjs-general-forecolor": "rgba(0, 0, 0, 0.91)",
14 | "--sjs-general-forecolor-light": "rgba(0, 0, 0, 0.45)",
15 | "--sjs-general-dim-forecolor": "rgba(0, 0, 0, 0.91)",
16 | "--sjs-general-dim-forecolor-light": "rgba(0, 0, 0, 0.45)",
17 | "--sjs-primary-backcolor": "rgba(0, 0, 0, 1)",
18 | "--sjs-primary-backcolor-light": "rgba(0, 0, 0, 0.1)",
19 | "--sjs-primary-backcolor-dark": "rgba(-15, -15, -15, 1)",
20 | "--sjs-primary-forecolor": "rgba(255, 255, 255, 1)",
21 | "--sjs-primary-forecolor-light": "rgba(255, 255, 255, 0.25)",
22 | "--sjs-base-unit": "4px",
23 | "--sjs-corner-radius": "4px",
24 | "--sjs-secondary-backcolor": "rgba(255, 152, 20, 1)",
25 | "--sjs-secondary-backcolor-light": "rgba(255, 152, 20, 0.1)",
26 | "--sjs-secondary-backcolor-semi-light": "rgba(255, 152, 20, 0.25)",
27 | "--sjs-secondary-forecolor": "rgba(255, 255, 255, 1)",
28 | "--sjs-secondary-forecolor-light": "rgba(255, 255, 255, 0.25)",
29 | "--sjs-shadow-small": "inset 0px 0px 0px 1px rgba(0, 0, 0, 0.2)",
30 | "--sjs-shadow-small-reset": "inset 0px 0px 0px 0px rgba(0, 0, 0, 0.2)",
31 | "--sjs-shadow-medium": "0px 2px 6px 0px rgba(0, 0, 0, 0.1)",
32 | "--sjs-shadow-large": "0px 8px 16px 0px rgba(0, 0, 0, 0.1)",
33 | "--sjs-shadow-inner": "0px 1px 0px 0px rgba(0, 0, 0, 0.2)",
34 | "--sjs-shadow-inner-reset": "0px 0px 0px 0px rgba(0, 0, 0, 0.2)",
35 | "--sjs-border-light": "rgba(0, 0, 0, 0.25)",
36 | "--sjs-border-default": "rgba(0, 0, 0, 0.25)",
37 | "--sjs-border-inside": "rgba(0, 0, 0, 0.16)",
38 | "--sjs-special-red": "rgba(229, 10, 62, 1)",
39 | "--sjs-special-red-light": "rgba(229, 10, 62, 0.1)",
40 | "--sjs-special-red-forecolor": "rgba(255, 255, 255, 1)",
41 | "--sjs-special-green": "rgba(25, 179, 148, 1)",
42 | "--sjs-special-green-light": "rgba(25, 179, 148, 0.1)",
43 | "--sjs-special-green-forecolor": "rgba(255, 255, 255, 1)",
44 | "--sjs-special-blue": "rgba(67, 127, 217, 1)",
45 | "--sjs-special-blue-light": "rgba(67, 127, 217, 0.1)",
46 | "--sjs-special-blue-forecolor": "rgba(255, 255, 255, 1)",
47 | "--sjs-special-yellow": "rgba(255, 152, 20, 1)",
48 | "--sjs-special-yellow-light": "rgba(255, 152, 20, 0.1)",
49 | "--sjs-special-yellow-forecolor": "rgba(255, 255, 255, 1)",
50 | "--sjs-article-font-xx-large-textDecoration": "none",
51 | "--sjs-article-font-xx-large-fontWeight": "700",
52 | "--sjs-article-font-xx-large-fontStyle": "normal",
53 | "--sjs-article-font-xx-large-fontStretch": "normal",
54 | "--sjs-article-font-xx-large-letterSpacing": "0",
55 | "--sjs-article-font-xx-large-lineHeight": "64px",
56 | "--sjs-article-font-xx-large-paragraphIndent": "0px",
57 | "--sjs-article-font-xx-large-textCase": "none",
58 | "--sjs-article-font-x-large-textDecoration": "none",
59 | "--sjs-article-font-x-large-fontWeight": "700",
60 | "--sjs-article-font-x-large-fontStyle": "normal",
61 | "--sjs-article-font-x-large-fontStretch": "normal",
62 | "--sjs-article-font-x-large-letterSpacing": "0",
63 | "--sjs-article-font-x-large-lineHeight": "56px",
64 | "--sjs-article-font-x-large-paragraphIndent": "0px",
65 | "--sjs-article-font-x-large-textCase": "none",
66 | "--sjs-article-font-large-textDecoration": "none",
67 | "--sjs-article-font-large-fontWeight": "700",
68 | "--sjs-article-font-large-fontStyle": "normal",
69 | "--sjs-article-font-large-fontStretch": "normal",
70 | "--sjs-article-font-large-letterSpacing": "0",
71 | "--sjs-article-font-large-lineHeight": "40px",
72 | "--sjs-article-font-large-paragraphIndent": "0px",
73 | "--sjs-article-font-large-textCase": "none",
74 | "--sjs-article-font-medium-textDecoration": "none",
75 | "--sjs-article-font-medium-fontWeight": "700",
76 | "--sjs-article-font-medium-fontStyle": "normal",
77 | "--sjs-article-font-medium-fontStretch": "normal",
78 | "--sjs-article-font-medium-letterSpacing": "0",
79 | "--sjs-article-font-medium-lineHeight": "32px",
80 | "--sjs-article-font-medium-paragraphIndent": "0px",
81 | "--sjs-article-font-medium-textCase": "none",
82 | "--sjs-article-font-default-textDecoration": "none",
83 | "--sjs-article-font-default-fontWeight": "400",
84 | "--sjs-article-font-default-fontStyle": "normal",
85 | "--sjs-article-font-default-fontStretch": "normal",
86 | "--sjs-article-font-default-letterSpacing": "0",
87 | "--sjs-article-font-default-lineHeight": "28px",
88 | "--sjs-article-font-default-paragraphIndent": "0px",
89 | "--sjs-article-font-default-textCase": "none",
90 | "--sjs-article-font-xx-large-fontSize": "64px",
91 | "--sjs-article-font-x-large-fontSize": "48px",
92 | "--sjs-article-font-large-fontSize": "32px",
93 | "--sjs-article-font-medium-fontSize": "24px",
94 | "--sjs-article-font-default-fontSize": "16px",
95 | "--sjs-editor-background": "rgba(249, 249, 249, 1)",
96 | "--sjs-editorpanel-backcolor": "rgba(249, 249, 249, 0)",
97 | "--sjs-editorpanel-hovercolor": "rgba(243, 243, 243, 1)",
98 | "--sjs-editorpanel-cornerRadius": "0px",
99 | "--sjs-font-size": "16px",
100 | "--font-family": "Open Sans",
101 | "--sjs-font-editorfont-color": "rgba(0, 0, 0, 1)",
102 | "--sjs-font-editorfont-placeholdercolor": "rgba(0, 0, 0, 0.35)",
103 | "--sjs-font-surveytitle-size": "24px",
104 | "--sjs-font-pagetitle-color": "rgba(0, 0, 0, 0.9)",
105 | "--sjs-font-questiontitle-color": "rgba(0, 0, 0, 0.9)",
106 | "--sjs-font-questiondescription-color": "rgba(0, 0, 0, 0.35)",
107 | "--sjs-header-backcolor": "transparent",
108 | "--sjs-font-headerdescription-color": "rgba(0, 0, 0, 0.9)",
109 | "--sjs-font-headerdescription-weight": "700",
110 | "--sjs-font-headerdescription-size": "14px"
111 | },
112 | "themeName": "default",
113 | "colorPalette": "light",
114 | "header": {
115 | "height": 176,
116 | "inheritWidthFrom": "survey",
117 | "textAreaWidth": 360,
118 | "overlapEnabled": false,
119 | "backgroundImageOpacity": 1,
120 | "backgroundImageFit": "cover",
121 | "logoPositionX": "right",
122 | "logoPositionY": "middle",
123 | "titlePositionX": "right",
124 | "titlePositionY": "middle",
125 | "descriptionPositionX": "right",
126 | "descriptionPositionY": "middle"
127 | },
128 | "headerView": "advanced"
129 | };
--------------------------------------------------------------------------------
/src/components/ApplicationForm/theme.ts:
--------------------------------------------------------------------------------
1 | export const themeJson = {
2 | "backgroundImage": "https://api.surveyjs.io/private/Surveys/files?name=fff9c109-c767-467f-b0f7-f8f1e4f5b53a",
3 | "backgroundImageFit": "cover",
4 | "backgroundImageAttachment": "fixed",
5 | "backgroundOpacity": 1,
6 | "isPanelless": true,
7 | "cssVariables": {
8 | "--sjs-general-backcolor": "rgba(255, 255, 255, 1)",
9 | "--sjs-general-backcolor-dark": "rgba(248, 248, 248, 1)",
10 | "--sjs-general-backcolor-dim": "rgba(255, 255, 255, 1)",
11 | "--sjs-general-backcolor-dim-light": "rgba(249, 249, 249, 0)",
12 | "--sjs-general-backcolor-dim-dark": "rgba(243, 243, 243, 1)",
13 | "--sjs-general-forecolor": "rgba(0, 0, 0, 0.91)",
14 | "--sjs-general-forecolor-light": "rgba(0, 0, 0, 0.45)",
15 | "--sjs-general-dim-forecolor": "rgba(0, 0, 0, 0.91)",
16 | "--sjs-general-dim-forecolor-light": "rgba(0, 0, 0, 0.45)",
17 | "--sjs-primary-backcolor": "rgba(0, 0, 0, 1)",
18 | "--sjs-primary-backcolor-light": "rgba(0, 0, 0, 0.1)",
19 | "--sjs-primary-backcolor-dark": "rgba(-15, -15, -15, 1)",
20 | "--sjs-primary-forecolor": "rgba(255, 255, 255, 1)",
21 | "--sjs-primary-forecolor-light": "rgba(255, 255, 255, 0.25)",
22 | "--sjs-base-unit": "4px",
23 | "--sjs-corner-radius": "4px",
24 | "--sjs-secondary-backcolor": "rgba(255, 152, 20, 1)",
25 | "--sjs-secondary-backcolor-light": "rgba(255, 152, 20, 0.1)",
26 | "--sjs-secondary-backcolor-semi-light": "rgba(255, 152, 20, 0.25)",
27 | "--sjs-secondary-forecolor": "rgba(255, 255, 255, 1)",
28 | "--sjs-secondary-forecolor-light": "rgba(255, 255, 255, 0.25)",
29 | "--sjs-shadow-small": "inset 0px 0px 0px 1px rgba(0, 0, 0, 0.2)",
30 | "--sjs-shadow-small-reset": "inset 0px 0px 0px 0px rgba(0, 0, 0, 0.2)",
31 | "--sjs-shadow-medium": "0px 2px 6px 0px rgba(0, 0, 0, 0.1)",
32 | "--sjs-shadow-large": "0px 8px 16px 0px rgba(0, 0, 0, 0.1)",
33 | "--sjs-shadow-inner": "0px 1px 0px 0px rgba(0, 0, 0, 0.2)",
34 | "--sjs-shadow-inner-reset": "0px 0px 0px 0px rgba(0, 0, 0, 0.2)",
35 | "--sjs-border-light": "rgba(0, 0, 0, 0.25)",
36 | "--sjs-border-default": "rgba(0, 0, 0, 0.25)",
37 | "--sjs-border-inside": "rgba(0, 0, 0, 0.16)",
38 | "--sjs-special-red": "rgba(229, 10, 62, 1)",
39 | "--sjs-special-red-light": "rgba(229, 10, 62, 0.1)",
40 | "--sjs-special-red-forecolor": "rgba(255, 255, 255, 1)",
41 | "--sjs-special-green": "rgba(25, 179, 148, 1)",
42 | "--sjs-special-green-light": "rgba(25, 179, 148, 0.1)",
43 | "--sjs-special-green-forecolor": "rgba(255, 255, 255, 1)",
44 | "--sjs-special-blue": "rgba(67, 127, 217, 1)",
45 | "--sjs-special-blue-light": "rgba(67, 127, 217, 0.1)",
46 | "--sjs-special-blue-forecolor": "rgba(255, 255, 255, 1)",
47 | "--sjs-special-yellow": "rgba(255, 152, 20, 1)",
48 | "--sjs-special-yellow-light": "rgba(255, 152, 20, 0.1)",
49 | "--sjs-special-yellow-forecolor": "rgba(255, 255, 255, 1)",
50 | "--sjs-article-font-xx-large-textDecoration": "none",
51 | "--sjs-article-font-xx-large-fontWeight": "700",
52 | "--sjs-article-font-xx-large-fontStyle": "normal",
53 | "--sjs-article-font-xx-large-fontStretch": "normal",
54 | "--sjs-article-font-xx-large-letterSpacing": "0",
55 | "--sjs-article-font-xx-large-lineHeight": "64px",
56 | "--sjs-article-font-xx-large-paragraphIndent": "0px",
57 | "--sjs-article-font-xx-large-textCase": "none",
58 | "--sjs-article-font-x-large-textDecoration": "none",
59 | "--sjs-article-font-x-large-fontWeight": "700",
60 | "--sjs-article-font-x-large-fontStyle": "normal",
61 | "--sjs-article-font-x-large-fontStretch": "normal",
62 | "--sjs-article-font-x-large-letterSpacing": "0",
63 | "--sjs-article-font-x-large-lineHeight": "56px",
64 | "--sjs-article-font-x-large-paragraphIndent": "0px",
65 | "--sjs-article-font-x-large-textCase": "none",
66 | "--sjs-article-font-large-textDecoration": "none",
67 | "--sjs-article-font-large-fontWeight": "700",
68 | "--sjs-article-font-large-fontStyle": "normal",
69 | "--sjs-article-font-large-fontStretch": "normal",
70 | "--sjs-article-font-large-letterSpacing": "0",
71 | "--sjs-article-font-large-lineHeight": "40px",
72 | "--sjs-article-font-large-paragraphIndent": "0px",
73 | "--sjs-article-font-large-textCase": "none",
74 | "--sjs-article-font-medium-textDecoration": "none",
75 | "--sjs-article-font-medium-fontWeight": "700",
76 | "--sjs-article-font-medium-fontStyle": "normal",
77 | "--sjs-article-font-medium-fontStretch": "normal",
78 | "--sjs-article-font-medium-letterSpacing": "0",
79 | "--sjs-article-font-medium-lineHeight": "32px",
80 | "--sjs-article-font-medium-paragraphIndent": "0px",
81 | "--sjs-article-font-medium-textCase": "none",
82 | "--sjs-article-font-default-textDecoration": "none",
83 | "--sjs-article-font-default-fontWeight": "400",
84 | "--sjs-article-font-default-fontStyle": "normal",
85 | "--sjs-article-font-default-fontStretch": "normal",
86 | "--sjs-article-font-default-letterSpacing": "0",
87 | "--sjs-article-font-default-lineHeight": "28px",
88 | "--sjs-article-font-default-paragraphIndent": "0px",
89 | "--sjs-article-font-default-textCase": "none",
90 | "--sjs-article-font-xx-large-fontSize": "64px",
91 | "--sjs-article-font-x-large-fontSize": "48px",
92 | "--sjs-article-font-large-fontSize": "32px",
93 | "--sjs-article-font-medium-fontSize": "24px",
94 | "--sjs-article-font-default-fontSize": "16px",
95 | "--sjs-editor-background": "rgba(249, 249, 249, 1)",
96 | "--sjs-editorpanel-backcolor": "rgba(249, 249, 249, 0)",
97 | "--sjs-editorpanel-hovercolor": "rgba(243, 243, 243, 1)",
98 | "--sjs-editorpanel-cornerRadius": "0px",
99 | "--sjs-font-size": "16px",
100 | "--font-family": "Open Sans",
101 | "--sjs-font-editorfont-color": "rgba(0, 0, 0, 1)",
102 | "--sjs-font-editorfont-placeholdercolor": "rgba(0, 0, 0, 0.35)",
103 | "--sjs-font-surveytitle-size": "24px",
104 | "--sjs-font-pagetitle-color": "rgba(0, 0, 0, 0.9)",
105 | "--sjs-font-questiontitle-color": "rgba(0, 0, 0, 0.9)",
106 | "--sjs-font-questiondescription-color": "rgba(0, 0, 0, 0.35)",
107 | "--sjs-header-backcolor": "transparent",
108 | "--sjs-font-headerdescription-color": "rgba(0, 0, 0, 0.9)",
109 | "--sjs-font-headerdescription-weight": "700",
110 | "--sjs-font-headerdescription-size": "14px"
111 | },
112 | "themeName": "default",
113 | "colorPalette": "light",
114 | "header": {
115 | "height": 176,
116 | "inheritWidthFrom": "survey",
117 | "textAreaWidth": 360,
118 | "overlapEnabled": false,
119 | "backgroundImageOpacity": 1,
120 | "backgroundImageFit": "cover",
121 | "logoPositionX": "right",
122 | "logoPositionY": "middle",
123 | "titlePositionX": "right",
124 | "titlePositionY": "middle",
125 | "descriptionPositionX": "right",
126 | "descriptionPositionY": "middle"
127 | },
128 | "headerView": "advanced"
129 | }
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ))
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ))
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ))
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 |
97 | ))
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ))
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ))
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ))
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | )
186 | }
187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuTrigger,
192 | DropdownMenuContent,
193 | DropdownMenuItem,
194 | DropdownMenuCheckboxItem,
195 | DropdownMenuRadioItem,
196 | DropdownMenuLabel,
197 | DropdownMenuSeparator,
198 | DropdownMenuShortcut,
199 | DropdownMenuGroup,
200 | DropdownMenuPortal,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuRadioGroup,
205 | }
206 |
--------------------------------------------------------------------------------
/src/app/affordability/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { Button } from "@/components/ui/button";
5 | import { Input } from "@/components/ui/input";
6 | import { Label } from "@/components/ui/label";
7 | import {
8 | Card,
9 | CardContent,
10 | CardDescription,
11 | CardFooter,
12 | CardHeader,
13 | CardTitle,
14 | } from "@/components/ui/card";
15 | import { Slider } from "@/components/ui/slider";
16 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
17 | import Container from "@/components/Container";
18 |
19 | // Define a type for the results
20 | interface Results {
21 | maxMortgage: string;
22 | monthlyPayment: string;
23 | downPaymentPercentage: string;
24 | }
25 |
26 | export default function MortgageCalculator() {
27 | const [annualIncome, setAnnualIncome] = useState(50000);
28 | const [monthlyDebts, setMonthlyDebts] = useState(500);
29 | const [downPayment, setDownPayment] = useState(20000);
30 | const [interestRate, setInterestRate] = useState(3.5);
31 | const [loanTerm, setLoanTerm] = useState(30);
32 | const [creditScore, setCreditScore] = useState(700);
33 | const [results, setResults] = useState(null);
34 |
35 | const calculateAffordability = () => {
36 | // Monthly income
37 | const monthlyIncome = annualIncome / 12;
38 |
39 | // Maximum monthly payment (using 28/36 rule)
40 | const maxMonthlyPayment = Math.min(
41 | monthlyIncome * 0.28,
42 | monthlyIncome * 0.36 - monthlyDebts
43 | );
44 |
45 | // Calculate maximum loan amount
46 | const monthlyInterestRate = interestRate / 100 / 12;
47 | const numberOfPayments = loanTerm * 12;
48 | const maxLoanAmount =
49 | (maxMonthlyPayment / monthlyInterestRate) *
50 | (1 - Math.pow(1 + monthlyInterestRate, -numberOfPayments));
51 |
52 | // Total mortgage amount (including down payment)
53 | const totalMortgage = maxLoanAmount + downPayment;
54 |
55 | // Monthly mortgage payment
56 | const monthlyMortgagePayment =
57 | (maxLoanAmount *
58 | monthlyInterestRate *
59 | Math.pow(1 + monthlyInterestRate, numberOfPayments)) /
60 | (Math.pow(1 + monthlyInterestRate, numberOfPayments) - 1);
61 |
62 | setResults({
63 | maxMortgage: totalMortgage.toFixed(2),
64 | monthlyPayment: monthlyMortgagePayment.toFixed(2),
65 | downPaymentPercentage: ((downPayment / totalMortgage) * 100).toFixed(2),
66 | });
67 | };
68 |
69 | return (
70 |
71 |
72 |
73 |
74 |
75 | Mortgage Affordability Calculator
76 |
77 |
78 | Calculate how much home you can afford based on your financial
79 | situation.
80 |
81 |
82 |
83 |
84 |
85 |
86 | Basic Info
87 |
88 |
89 | Advanced Options
90 |
91 |
92 |
93 |
131 |
132 |
133 |
134 |
135 |
136 | Interest Rate (%)
137 |
138 |
setInterestRate(value[0])}
145 | />
146 |
147 | {interestRate.toFixed(1)}%
148 |
149 |
150 |
151 |
152 | Loan Term (years)
153 |
154 |
setLoanTerm(value[0])}
161 | />
162 |
163 | {loanTerm} years
164 |
165 |
166 |
167 |
168 | Credit Score
169 |
170 |
setCreditScore(value[0])}
177 | />
178 |
179 | {creditScore}
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
192 | Calculate Affordability
193 |
194 | {results && (
195 |
196 |
Results
197 |
198 |
199 | Maximum Mortgage:
200 |
201 |
202 |
203 |
204 |
205 |
${results.maxMortgage}
206 |
Monthly Payment:
207 |
${results.monthlyPayment}
208 |
Down Payment:
209 |
210 | {results.downPaymentPercentage}%
211 |
212 |
213 |
214 | )}
215 |
216 |
217 |
218 |
219 | );
220 | }
221 |
--------------------------------------------------------------------------------
/src/data/json.ts:
--------------------------------------------------------------------------------
1 | export const json = {
2 | "title": "Mortgage Loan Application Form",
3 | "pages": [{
4 | "name": "overview",
5 | "title": "Overview of your Mortgage Application",
6 | "elements": [{
7 | "type": "radiogroup",
8 | "name": "loan-purpose",
9 | "title": "Is your mortgage application for?",
10 | "choices": [{
11 | "value": "remortgage",
12 | "text": "Remortgage"
13 | }, {
14 | "value": "equity-transfer",
15 | "text": "Transfer of Equity"
16 | }, {
17 | "value": "house-purchase",
18 | "text": "House purchase"
19 | }, {
20 | "value": "term-extension",
21 | "text": "Term Extension"
22 | }, {
23 | "value": "further-advance",
24 | "text": "Further Advance"
25 | }],
26 | "colCount": 2
27 | }, {
28 | "type": "boolean",
29 | "name": "guarantor",
30 | "startWithNewLine": false,
31 | "title": "Is there a guarantor for this application?"
32 | }, {
33 | "type": "radiogroup",
34 | "name": "property-use",
35 | "title": "Is this property to be used for?",
36 | "choices": [{
37 | "value": "residential",
38 | "text": "Residential"
39 | }, {
40 | "value": "buy-to-let",
41 | "text": "Buy to let"
42 | }, {
43 | "value": "holiday-let",
44 | "text": "Holiday let"
45 | }]
46 | }, {
47 | "type": "boolean",
48 | "name": "holiday-let-time-period",
49 | "visibleIf": "{property-use} = 'holiday-let'",
50 | "startWithNewLine": false,
51 | "title": "If this is an application for a holiday let, do you intend to personally use the property for more than 60 days per annum?"
52 | }]
53 | }, {
54 | "name": "property-details",
55 | "title": "Property to be mortgaged",
56 | "elements": [{
57 | "type": "text",
58 | "name": "street-address",
59 | "title": "Street address"
60 | }, {
61 | "type": "text",
62 | "name": "city",
63 | "title": "City/Town"
64 | }, {
65 | "type": "text",
66 | "name": "zip",
67 | "startWithNewLine": false,
68 | "title": "Zip Code"
69 | }, {
70 | "type": "dropdown",
71 | "name": "country",
72 | "startWithNewLine": false,
73 | "title": "Country",
74 | "choicesByUrl": {
75 | "url": "https://surveyjs.io/api/CountriesExample"
76 | }
77 | }, {
78 | "type": "boolean",
79 | "name": "used-as-main-residence",
80 | "title": "Is this property to be used as your main residence?"
81 | }, {
82 | "type": "boolean",
83 | "name": "is-already-mortgaged",
84 | "visibleIf": "({loan-purpose} = 'house-purchase') and ('property-use'=='residental')",
85 | "startWithNewLine": false,
86 | "title": "Is there a mortgage on this property?"
87 | }, {
88 | "type": "boolean",
89 | "name": "used-for-business",
90 | "startWithNewLine": false,
91 | "title": "Will any part of the property be used for business purposes?"
92 | }]
93 | }, {
94 | "name": "personal-info",
95 | "title": "Personal Information",
96 | "elements": [
97 | {
98 | "type": "paneldynamic",
99 | "name": "applicant-info",
100 | "titleLocation": "hidden",
101 | "defaultValue": [
102 | {}
103 | ],
104 | "templateTitle": "Applicant #{panelIndex}",
105 | "panelAddText": "Add an applicant",
106 | "templateElements": [
107 | {
108 | "type": "multipletext",
109 | "name": "full-name",
110 | "items": [
111 | {
112 | "name": "first-name",
113 | "title": "First name"
114 | },
115 | {
116 | "name": "last-name",
117 | "title": "Last name"
118 | }
119 | ]
120 | },
121 | {
122 | "type": "multipletext",
123 | "name": "birth-info",
124 | "startWithNewLine": false,
125 | "items": [
126 | {
127 | "name": "birthplace",
128 | "title": "Place of birth"
129 | },
130 | {
131 | "name": "birthdate",
132 | "inputType": "date",
133 | "title": "Date of birth"
134 | }
135 | ]
136 | },
137 | {
138 | "type": "text",
139 | "name": "phone",
140 | "title": "Phone number",
141 | "titleLocation": "top",
142 | "maskType": "pattern",
143 | "maskSettings": {
144 | "saveMaskedValue": true,
145 | "pattern": "+9 (999) 999-99-99"
146 | }
147 | },
148 | {
149 | "type": "text",
150 | "name": "street-address",
151 | "startWithNewLine": false,
152 | "title": "Street address",
153 | "titleLocation": "top"
154 | },
155 | {
156 | "type": "text",
157 | "name": "city",
158 | "title": "City/Town",
159 | "titleLocation": "top"
160 | },
161 | {
162 | "type": "text",
163 | "name": "zip",
164 | "startWithNewLine": false,
165 | "title": "Zip Code",
166 | "titleLocation": "top"
167 | },
168 | {
169 | "type": "dropdown",
170 | "name": "country",
171 | "startWithNewLine": false,
172 | "title": "Country",
173 | "titleLocation": "top",
174 | "choicesByUrl": {
175 | "url": "https://surveyjs.io/api/CountriesExample"
176 | }
177 | }
178 | ]
179 | },
180 | {
181 | "name": "documents",
182 | "title": "Documents",
183 | "elements": [
184 | {
185 | "type": "matrixdropdown",
186 | "name": "ids",
187 | "title": "Select two documents verifying your identity and upload their scan copies in PDF format.",
188 | "titleLocation": "top",
189 | "columns": [
190 | {
191 | "name": "id-type",
192 | "title": "ID type",
193 | "cellType": "dropdown",
194 | "choices": [
195 | "State identification (ID) card",
196 | "Driver license",
197 | "US passport or passport card",
198 | "US military card (front and back)",
199 | "Military dependent's ID card (front and back)",
200 | "Permanent Resident Card",
201 | "Certificate of Citizenship",
202 | "Certificate of Naturalization"
203 | ]
204 | },
205 | {
206 | "name": "expiration-date",
207 | "title": "Expiration date",
208 | "cellType": "text",
209 | "inputType": "date"
210 | }
211 | ],
212 | "rows": [
213 | {
214 | "value": "first-id-info",
215 | "text": "#1"
216 | },
217 | {
218 | "value": "second-id-info",
219 | "text": "#2"
220 | }
221 | ],
222 | "rowTitleWidth": "0px"
223 | },
224 | {
225 | "type": "file",
226 | "name": "first-id",
227 | "titleLocation": "hidden",
228 | "acceptedTypes": "application/pdf"
229 | },
230 | {
231 | "type": "file",
232 | "name": "second-id",
233 | "startWithNewLine": false,
234 | "titleLocation": "hidden",
235 | "acceptedTypes": "application/pdf"
236 | }
237 | ]
238 | }
239 | ]
240 | }, {
241 | "name": "employment-details",
242 | "title": "Employment details",
243 | "elements": [{
244 | "type": "paneldynamic",
245 | "name": "employer",
246 | "startWithNewLine": false,
247 | "titleLocation": "hidden",
248 | "panelAddText": "Add an employer",
249 | "defaultValue": [
250 | {}
251 | ],
252 | "templateElements": [{
253 | "type": "comment",
254 | "name": "organization",
255 | "title": "Name of Organization",
256 | "titleLocation": "top",
257 | "placeholder": "Enter the name of your current employer"
258 | }, {
259 | "type": "multipletext",
260 | "name": "occupation",
261 | "items": [{
262 | "name": "position",
263 | "placeholder": "Enter your current position in the company",
264 | "title": "Occupation"
265 | }, {
266 | "name": "employment-date",
267 | "inputType": "date",
268 | "title": "Employed since"
269 | }]
270 | }, {
271 | "type": "panel",
272 | "name": "income-panel",
273 | "elements": [{
274 | "type": "multipletext",
275 | "name": "income",
276 | "title": "Monthly Income in USD",
277 | "titleLocation": "top",
278 | "items": [{
279 | "name": "basic-salary",
280 | "title": "Basic salary"
281 | }, {
282 | "name": "guaranteed-bonus",
283 | "title": "Guaranteed bonus"
284 | }, {
285 | "name": "nonguaranteed-bonus",
286 | "title": "Non-guaranteed bonus"
287 | }]
288 | }]
289 | }, {
290 | "type": "panel",
291 | "name": "other-income-panel",
292 | "elements": [{
293 | "type": "boolean",
294 | "name": "have-other-income-sources",
295 | "title": "Any other regular income?",
296 | "titleLocation": "top"
297 | }, {
298 | "type": "text",
299 | "name": "other-income-sources",
300 | "placeholder": "Please specify..."
301 | }],
302 | "startWithNewLine": false
303 | }, {
304 | "type": "file",
305 | "name": "employment-verification-letter",
306 | "title": "Letter of Employment Verification",
307 | "description": "Please upload a letter of employment verification signed by your current employer.",
308 | "titleLocation": "top"
309 | }]
310 | }]
311 | }, {
312 | "name": "loan-details",
313 | "title": "Requested Loan Details",
314 | "elements": [{
315 | "type": "text",
316 | "name": "loan-amount",
317 | "title": "Requested loan amount in USD",
318 | "maskType": "currency",
319 | "maskSettings": {
320 | "prefix": "$"
321 | },
322 | "placeholder": "$1,000,000"
323 | }, {
324 | "type": "text",
325 | "name": "loan-tenure",
326 | "startWithNewLine": false,
327 | "title": "Loan tenure in years",
328 | "inputType": "number",
329 | "min": 1,
330 | "max": 25
331 | }]
332 | }],
333 | "showQuestionNumbers": false,
334 | "completeText": "Submit",
335 | "widthMode": "static",
336 | "width": "1200px"
337 | };
--------------------------------------------------------------------------------