├── src ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── api │ │ └── notify-view │ │ │ └── route.ts │ └── page.tsx └── components │ ├── ProgressBar.tsx │ └── SocialLinks.tsx ├── postcss.config.mjs ├── next.config.ts ├── vercel.json ├── .env.example ├── eslint.config.mjs ├── .gitignore ├── tsconfig.json ├── package.json ├── LICENSE ├── ResumeConfig.ts └── README.md /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VishalRMahajan/ResumeRedirect/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "/(.*)", 5 | "headers": [ 6 | { 7 | "key": "X-Robots-Tag", 8 | "value": "index, follow" 9 | } 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | // Create a .env file in the root directory of your project and fill in the values below with your own. 2 | // EMAIL_ USER is the email address you want to use to send notifications. 3 | // EMAIL_PASSWORD is the password for that email address. (Make sure to use an app password if using Gmail.) 4 | // NOTIFICATION_EMAIL is the email address where you want to receive notifications. 5 | EMAIL_USER= 6 | EMAIL_PASSWORD= 7 | NOTIFICATION_EMAIL= 8 | NEXT_PUBLIC_RESUME_URL= -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resume-redirect", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "framer-motion": "^12.23.0", 13 | "next": "15.3.8", 14 | "nodemailer": "^6.10.1", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0", 17 | "react-icons": "^5.5.0" 18 | }, 19 | "devDependencies": { 20 | "@eslint/eslintrc": "^3", 21 | "@tailwindcss/postcss": "^4", 22 | "@types/node": "^20", 23 | "@types/nodemailer": "^6.4.17", 24 | "@types/react": "^19", 25 | "@types/react-dom": "^19", 26 | "eslint": "^9", 27 | "eslint-config-next": "15.3.1", 28 | "tailwindcss": "^4", 29 | "typescript": "^5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Vishal Mahajan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import { ResumeConfig } from "../../ResumeConfig"; 5 | 6 | const geistSans = Geist({ 7 | variable: "--font-geist-sans", 8 | subsets: ["latin"], 9 | display: "swap", 10 | }); 11 | 12 | const geistMono = Geist_Mono({ 13 | variable: "--font-geist-mono", 14 | subsets: ["latin"], 15 | display: "swap", 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: ResumeConfig.seo.title, 20 | description: ResumeConfig.seo.description, 21 | keywords: [ 22 | "resume", 23 | "portfolio", 24 | ResumeConfig.name, 25 | ResumeConfig.designation, 26 | ], 27 | robots: { 28 | index: true, 29 | follow: true, 30 | }, 31 | }; 32 | 33 | export default function RootLayout({ 34 | children, 35 | }: Readonly<{ 36 | children: React.ReactNode; 37 | }>) { 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | {children} 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | type ProgressBarProps = { 6 | onComplete: () => void; 7 | disabled?: boolean; 8 | accelerated?: boolean; 9 | }; 10 | 11 | export default function ProgressBar({ 12 | onComplete, 13 | disabled = false, 14 | accelerated = false, 15 | }: ProgressBarProps) { 16 | const [progress, setProgress] = useState(0); 17 | 18 | useEffect(() => { 19 | if (disabled) return; 20 | 21 | const increment = accelerated ? 4 : 2; 22 | const interval = accelerated ? 30 : 50; 23 | 24 | const timer = setInterval(() => { 25 | setProgress((prev) => { 26 | if (prev >= 98) { 27 | clearInterval(timer); 28 | setTimeout(() => { 29 | onComplete(); 30 | }, 100); 31 | return 100; 32 | } 33 | return prev + increment; 34 | }); 35 | }, interval); 36 | 37 | return () => clearInterval(timer); 38 | }, [onComplete, disabled, accelerated]); 39 | 40 | return ( 41 |
42 |
43 | Redirecting to resume... 44 | {progress}% 45 |
46 |
47 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /ResumeConfig.ts: -------------------------------------------------------------------------------- 1 | export const ResumeConfig = { 2 | // URL to the resume file (PDF, DOCX, etc.) 3 | // This URL should be publicly accessible for the redirect to work 4 | // You can use services like Google Drive, Dropbox, or your own server to host the file 5 | // Make sure to set the file permissions to public or anyone with the link can view 6 | resumeUrl: process.env.NEXT_PUBLIC_RESUME_URL, 7 | 8 | // Personal Information - these will be displayed on the page 9 | name: "VishalRMahajan", 10 | designation: "Software Development Engineer", 11 | 12 | // Social Media Links - these will be displayed as icons on the page 13 | // You can add or remove social media links as needed 14 | // Blank links Icon will not be displayed on the page 15 | socialMedia: { 16 | github: "https://github.com/VishalRMahajan", 17 | linkedin: "https://linkedin.com/in/VishalRMahajan", 18 | portfolio: "https://vishalrmahajan.in", 19 | twitter: "https://x.com/VishalRMahajan", 20 | email: "mailto:vism06@gmail.com", 21 | }, 22 | 23 | // Search Engine Optimization (SEO) - these will be used for the page metadata 24 | // You can customize the title and description for better visibility on search engines 25 | seo: { 26 | title: "Vishal Mahajan | Developer Resume", 27 | description: "Resume of Vishal Mahajan (VishalRMahajan).", 28 | }, 29 | // Email notification settings 30 | // This will send an email notification when the resume link is opened 31 | // You can set this to false if you don't want to receive email notifications 32 | notifications: { 33 | sendMail: true, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/SocialLinks.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FaGithub } from "react-icons/fa"; 4 | import { FaTwitter } from "react-icons/fa"; 5 | import { FaEnvelope } from "react-icons/fa"; 6 | import { LiaLinkedinIn } from "react-icons/lia"; 7 | import { FaGlobe } from "react-icons/fa"; 8 | 9 | type SocialLinksProps = { 10 | socialMedia: { 11 | github?: string; 12 | linkedin?: string; 13 | portfolio?: string; 14 | email?: string; 15 | twitter?: string; 16 | }; 17 | }; 18 | 19 | export default function SocialLinks({ socialMedia }: SocialLinksProps) { 20 | const hasSocialLinks = Object.values(socialMedia).some((url) => url); 21 | 22 | if (!hasSocialLinks) return null; 23 | 24 | return ( 25 |
26 | {socialMedia.github && ( 27 | 33 | 34 | 35 | )} 36 | 37 | {socialMedia.linkedin && ( 38 | 44 | 45 | 46 | )} 47 | 48 | {socialMedia.portfolio && ( 49 | 55 | 56 | 57 | )} 58 | 59 | {socialMedia.twitter && ( 60 | 66 | 67 | 68 | )} 69 | 70 | {socialMedia.email && ( 71 | 77 | 78 | 79 | )} 80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | ResumeRedirect 5 |

6 | 7 | ResumeRedirect is a lightweight and modern solution for sharing your resume via a persistent link with built-in branding, fallback links, and optional email notifications. Perfect for developers who frequently update their resumes but want a single link that stays consistent. 🚀📄 8 | 9 | ## Demo 10 | 11 | 12 | 13 | 14 | https://github.com/user-attachments/assets/6f566a5e-778b-4161-8390-92b995f057ea 15 | 16 | 17 | 18 | ## Table of Contents 19 | 20 | - [Overview](#overview) 21 | - [Why ResumeRedirect?](#why-resumeredirect) 22 | - [Demo](#demo) 23 | - [Features](#features) 24 | - [Configuration](#configuration) 25 | - [Getting Started](#getting-started) 26 | - [Deployment](#deployment) 27 | - [Tech Stack](#tech-stack) 28 | - [Screenshots](#screenshots) 29 | - [License](#license) 30 | 31 | 32 | ## Overview 33 | 34 | You share your resume with someone but resumes evolve. With traditional links, any updates require resharing. ResumeRedirect solves that. 🔁 35 | 36 | Deploy once, share the link, and forget the rest. Any changes you make to your resume will automatically reflect at the same URL without your recipients needing a new link. Plus, a clean UI lets the viewer know the resume is loading and includes fallback contact links in case redirection fails. 💡 37 | 38 | 39 | ## Why ResumeRedirect? 40 | 41 | Let’s face it sending a direct link to your resume can be limiting: 42 | 43 | - You can't update your resume without updating the URL (unless you overwrite the file). 44 | - No branding or personal presence before redirect. 45 | - No way to track or log views (optionally). 46 | - No fallback if the file is unreachable. 47 | 48 | **ResumeRedirect** solves all these problems with a beautiful, minimal, and practical solution. Whether you’re a student, freelancer, or experienced engineer, it helps you present yourself better and stay in control of your resume link. 🧠💼 49 | 50 | 51 | ## Demo 52 | 53 | - Live: [resume.vishalrmahajan.in](https://resume.vishalrmahajan.in) 54 | 55 | 56 | ## Features 57 | 58 | - ✨ Auto-redirect with animated loading indicator 59 | - 🌙 Dark-themed, responsive business card UI 60 | - 📩 Email notifications when your resume is opened (configurable) 61 | - 🔧 Simple single-file configuration (`ResumeConfig.ts`) 62 | - 📈 SEO optimized for better discoverability 63 | - 📎 Fallback UI shows social/contact links + direct resume link if redirection fails 64 | - 🚀 One-click deploy on Vercel 65 | 66 | 67 | ## Configuration 68 | 69 | Configure your resume and profile data in `ResumeConfig.ts`: 70 | 71 | ```ts 72 | export const ResumeConfig = { 73 | resumeUrl: "https://drive.google.com/your-resume-link", 74 | name: "Your Name", 75 | designation: "Your Role", 76 | socialMedia: { 77 | github: "https://github.com/yourusername", 78 | linkedin: "https://linkedin.com/in/yourusername", 79 | portfolio: "https://yourportfolio.com", 80 | twitter: "https://x.com/yourhandle", 81 | email: "mailto:you@example.com", 82 | }, 83 | seo: { 84 | title: "Your Name | Resume", 85 | description: "Software Developer Resume - Your Name", 86 | }, 87 | notifications: { 88 | sendMail: true, 89 | }, 90 | }; 91 | ``` 92 | 93 | Environment variables in `.env`: 94 | 95 | ```env 96 | EMAIL_USER=you@example.com 97 | EMAIL_PASSWORD=your_app_password 98 | NOTIFICATION_EMAIL=notify@example.com 99 | ``` 100 | 101 | 102 | ## Getting Started 103 | 104 | ### 1. Clone the Repository 105 | 106 | ```bash 107 | git clone https://github.com/VishalRMahajan/ResumeRedirect.git 108 | cd ResumeRedirect 109 | ``` 110 | 111 | ### 2. Install Dependencies 112 | 113 | ```bash 114 | npm install 115 | ``` 116 | 117 | ### 3. Run the Project 118 | 119 | ```bash 120 | npm run dev 121 | ``` 122 | 123 | Please consider starring the repository if you find this useful 🌟 124 | 125 | 126 | ## Deployment 127 | 128 | Deploy with Vercel: 129 | 130 |

131 | 132 | Deploy to Vercel 133 | 134 |

135 | 136 | 137 | ## Tech Stack 138 | 139 |

140 | 141 |

142 | 143 | ## License 144 | 145 | This project is licensed under the [MIT License](./LICENSE). 146 | 147 |

148 | Built with ❤️ by Vishal Rajesh Mahajan 149 |

150 | -------------------------------------------------------------------------------- /src/app/api/notify-view/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import nodemailer from "nodemailer"; 3 | import { ResumeConfig } from "../../../../ResumeConfig"; 4 | 5 | export async function POST(request: Request) { 6 | try { 7 | const body = await request.json(); 8 | const { timestamp, sendMail = true, source = "direct" } = body; 9 | 10 | console.log(`Resume viewed at ${new Date(timestamp).toLocaleString()}`); 11 | 12 | if (!sendMail || !ResumeConfig.notifications.sendMail) { 13 | return NextResponse.json({ 14 | success: true, 15 | message: "View logged (email notifications disabled)", 16 | }); 17 | } 18 | 19 | if (!process.env.EMAIL_USER || !process.env.EMAIL_PASSWORD) { 20 | return NextResponse.json({ 21 | success: true, 22 | message: "Email credentials not configured", 23 | }); 24 | } 25 | 26 | const transporter = nodemailer.createTransport({ 27 | service: "gmail", 28 | auth: { 29 | user: process.env.EMAIL_USER, 30 | pass: process.env.EMAIL_PASSWORD, 31 | }, 32 | }); 33 | 34 | const date = new Date(timestamp); 35 | const formattedDate = date.toLocaleString("en-IN", { 36 | day: "numeric", 37 | month: "long", 38 | year: "numeric", 39 | hour: "numeric", 40 | minute: "numeric", 41 | hour12: true, 42 | timeZone: "Asia/Kolkata", 43 | }); 44 | 45 | const mailOptions = { 46 | from: process.env.EMAIL_USER, 47 | to: process.env.NOTIFICATION_EMAIL || process.env.EMAIL_USER, 48 | subject: "Someone viewed your resume 📄", 49 | html: ` 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 99 | 100 |
60 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 89 | 90 | 91 | 92 | 96 | 97 |
64 |

Resume Redirect

65 |

Good news! Someone just took a look at your resume. Here’s what we know:

66 | 67 |
72 |
73 |
74 |

Resume URL

75 | ${ResumeConfig.resumeUrl} 76 |
77 |
78 |

Viewed At

79 |

${formattedDate} IST

80 |
81 |
82 |

Source

83 |

${source}

84 |
85 |
86 | 87 | 88 |
93 |

94 | Resume Redirect • You can disable automated notifications by updating ResumeConfig.ts 95 |

98 |
101 | 102 | 103 | `, 104 | }; 105 | 106 | await transporter.sendMail(mailOptions); 107 | 108 | return NextResponse.json({ success: true }); 109 | } catch (error) { 110 | console.error("Failed to send notification email:", error); 111 | return NextResponse.json( 112 | { error: "Failed to send notification email" }, 113 | { status: 500 } 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState, useRef, Suspense } from "react"; 4 | import { ResumeConfig } from "../../ResumeConfig"; 5 | import { FaExternalLinkAlt } from "react-icons/fa"; 6 | import { useSearchParams } from "next/navigation"; 7 | 8 | import ProgressBar from "../components/ProgressBar"; 9 | import SocialLinks from "../components/SocialLinks"; 10 | 11 | interface SearchParamsHandlerProps { 12 | onSourceReceived: (source: string) => void; 13 | } 14 | 15 | function SearchParamsHandler({ onSourceReceived }: SearchParamsHandlerProps) { 16 | const searchParams = useSearchParams(); 17 | const source = searchParams.get("src") || "direct"; 18 | 19 | useEffect(() => { 20 | onSourceReceived(source); 21 | }, [source, onSourceReceived]); 22 | 23 | return null; 24 | } 25 | 26 | export default function Home() { 27 | const { resumeUrl, name, designation, socialMedia, notifications } = 28 | ResumeConfig; 29 | const [isRedirecting, setIsRedirecting] = useState(false); 30 | const [mounted, setMounted] = useState(false); 31 | const [source, setSource] = useState("direct"); 32 | const emailSentRef = useRef(false); 33 | 34 | useEffect(() => { 35 | setMounted(true); 36 | 37 | const link = document.createElement("link"); 38 | link.rel = "prefetch"; 39 | link.href = resumeUrl ?? ""; 40 | document.head.appendChild(link); 41 | 42 | const timer = setTimeout(() => { 43 | setIsRedirecting(true); 44 | }, 100); 45 | 46 | return () => { 47 | clearTimeout(timer); 48 | if (document.head.contains(link)) { 49 | document.head.removeChild(link); 50 | } 51 | }; 52 | }, [resumeUrl]); 53 | 54 | const handleRedirectComplete = () => { 55 | if (!emailSentRef.current && notifications?.sendMail !== false) { 56 | emailSentRef.current = true; 57 | 58 | const data = JSON.stringify({ 59 | timestamp: new Date().toISOString(), 60 | sendMail: notifications?.sendMail ?? true, 61 | source, 62 | }); 63 | 64 | if (navigator.sendBeacon) { 65 | navigator.sendBeacon("/api/notify-view", data); 66 | } else { 67 | fetch("/api/notify-view", { 68 | method: "POST", 69 | headers: { "Content-Type": "application/json" }, 70 | body: data, 71 | keepalive: true, 72 | }); 73 | } 74 | } 75 | 76 | window.location.href = resumeUrl ?? ""; 77 | }; 78 | 79 | if (!mounted) { 80 | return ( 81 |
82 |
83 |
84 | ); 85 | } 86 | 87 | return ( 88 |
89 | 90 | 91 | 92 | 93 |
94 |
95 | 96 |
97 |
98 |
99 |
100 |

101 | {name} 102 |

103 | 104 |
105 | 106 |

107 | {designation} 108 |

109 |
110 | 111 |
112 | 117 |
118 | 119 |
120 |

121 | Redirect not working? 122 |

123 | 127 | Open resume directly 128 | 129 |
130 | 131 |
132 | 133 |
134 |
135 |
136 | 137 | 144 |
145 |
146 | ); 147 | } 148 | --------------------------------------------------------------------------------