diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 6be54d1..0000000 --- a/.eslintignore +++ /dev/null @@ -1,13 +0,0 @@ -# artifacts -build -coverage - -# next.js -.next - -# dependencies -node_modules -pnpm-lock.yaml - -# assets -public \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index c43b8c6..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@next/next/recommended", - "plugin:prettier/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": "latest", - "project": ["tsconfig.json"], - "sourceType": "module" - }, - "plugins": ["react", "@typescript-eslint"], - "rules": { - "react/react-in-jsx-scope": "off", - "indent": "off", - "linebreak-style": ["error", "unix"], - "quotes": ["error", "double"], - "semi": ["error", "always"], - "no-console": "error", - "@typescript-eslint/no-unused-vars": "error", - "no-unused-vars": [ - "error", - { "argsIgnorePattern": "^_", "ignoreRestSiblings": true } - ], - "@typescript-eslint/no-inferrable-types": "off", - "no-restricted-syntax": [ - "error", - { - "selector": ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > LogicalExpression[operator!='??']", - "message": "Please use ternaries when writing JSX!" - } - ] - } -} diff --git a/.gitignore b/.gitignore index 9ac8bee..5ef6a52 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,12 @@ # dependencies /node_modules /.pnp -.pnp.js +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions # testing /coverage @@ -25,15 +30,12 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local -.env +# env files (can opt-in for committing if needed) +.env* # vercel .vercel # typescript *.tsbuildinfo +next-env.d.ts diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 6be54d1..0000000 --- a/.prettierignore +++ /dev/null @@ -1,13 +0,0 @@ -# artifacts -build -coverage - -# next.js -.next - -# dependencies -node_modules -pnpm-lock.yaml - -# assets -public \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 81ca21c..0000000 --- a/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "quoteProps": "consistent", - "singleQuote": false, - "semi": true, - "trailingComma": "all", - "bracketSpacing": true, - "jsxBracketSameLine": false, - "jsxSingleQuote": false, - "arrowParens": "avoid", - "tabWidth": 2 -} diff --git a/LICENSE b/LICENSE index 5d17eb2..2d7fe27 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Marin Heđeš +Copyright (c) 2025 Marin Heđeš Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 416117d..ff667ff 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,17 @@ # Personal website -- 💻 Available online at https://marinhedes.com and https://sincerelyfaust.com -- 💙 Written in TypeScript -- ⚛️ Built with Next.js -- 🎨 Styled with TailwindCSS -- 🚶‍♂️ Animated with Framer Motion -- 🔼 Deployed with Vercel +- Available at https://marinhedes.com and https://sincerelyfaust.com ## Development -To develop and locally test the website: - -### 1. Clone - -Clone this repository from GitHub: - -```sh -git clone https://github.com/SincerelyFaust/website -``` - -### 2. Install development tools: - -1. [Install Node.js and NPM](https://nodejs.org/en/download/package-manager/) -1. [Install PNPM](https://pnpm.io/installation) (recommended opposed to NPM/Yarn) -1. Code editor of your choice (I recommend [VSCode](https://code.visualstudio.com/)) - -### 3. Install dependencies: - -Install node modules required to run the website by running: +### 1. Install dependencies: ``` pnpm i ``` -### 4. Start the Next.js dev server: +### 2. Start the dev server: ``` pnpm dev -``` - -In a browser, load the page [localhost:3000](http://localhost:3000) and you should now be able to test the website while making your changes. -Next.js' dev server has hot reloading so no need to restart the instance when it's running! - -### 5. Build: - -After making your changes and verifying it all works in the dev server, furtherly test them out by building the website: - -``` -pnpm build -``` - -### 6. Start: - -``` -pnpm start -``` - -In a browser, load the page [localhost:3000](http://localhost:3000) and you should now be able to view the built website. - -## License - -Copyright @ 2023 - [Marin Heđeš](https://github.com/sincerelyfaust) - -This project is licensed under the [MIT license](LICENSE). +``` \ No newline at end of file diff --git a/app/cv/page.tsx b/app/cv/page.tsx new file mode 100644 index 0000000..9958fbf --- /dev/null +++ b/app/cv/page.tsx @@ -0,0 +1,60 @@ +import Link from "next/link"; + +export default function CVPage() { + const pdfPath = "/cv/cv.pdf"; + + return ( +
+
+
+
CV
+
+
+
+ +
+
+ + + + + + + + + +
+ +
+ +

+ Your browser can’t display PDFs inline.{" "} + + Open the PDF + {" "} + or{" "} + + download it + + . +

+
+
+ +
+

CV

+

PDF

+
+
+
+
+ ); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..a2dc41e --- /dev/null +++ b/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..5187936 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,90 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Marin Heđeš", + description: "Front-end and mobile developer", + keywords: [ + "portfolio", + "marin heđeš", + "faust", + "personal website", + "sincerelyfaust", + "founder", + "developer", + "frontend developer", + "mobile developer", + "software engineer", + ], + alternates: { + canonical: "https://www.marinhedes.com", + }, + openGraph: { + type: "website", + url: "https://www.marinhedes.com", + title: "Marin Heđeš", + siteName: "Marin Heđeš", + description: "Front-end and mobile developer", + images: [ + { + url: "https://avatars.githubusercontent.com/u/44751736?v=4", + }, + ], + }, + appleWebApp: { + capable: true, + title: "Marin Heđeš", + statusBarStyle: "default", + }, + manifest: "/site.webmanifest", + icons: { + icon: [ + { url: "/images/favicon/favicon.ico" }, + { + url: "/images/favicon/favicon-32x32.png", + sizes: "32x32", + type: "image/png", + }, + { + url: "/images/favicon/favicon-16x16.png", + sizes: "16x16", + type: "image/png", + }, + ], + apple: [{ url: "/images/favicon/apple-touch-icon.png", sizes: "180x180" }], + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + + {children} + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..37afb16 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,241 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useMemo, useState } from "react"; +import dayjs from "dayjs"; +import customParseFormat from "dayjs/plugin/customParseFormat"; + +dayjs.extend(customParseFormat); + +const BIRTHDATE_DMY = "23/02/2001"; + +const PROFILE = { + name: "Marin Heđeš", + role: "Front-End & Mobile Developer", + location: "Croatia, Slavonski Brod", + timezone: "Europe/Zagreb", + email: "hedesmarin@gmail.com", + avatar: "/images/profile/profile-picture.webp", + footerAppName: "MarinHedes.exe", +}; + +const SOCIAL = [ + { label: "LinkedIn", href: "/linkedin" }, + { label: "GitHub", href: "/github" }, + { label: "Twitter", href: "/twitter" }, + { label: "Instagram", href: "/instagram" }, +] as const; + +function calculateAge(birthDateString: string): number { + const today = dayjs(); + const birth = dayjs(birthDateString, "DD/MM/YYYY"); + return today.diff(birth, "year"); +} + +function getZagrebTimeString(d: Date) { + return new Intl.DateTimeFormat(["en-GB"], { + timeZone: PROFILE.timezone, + year: "numeric", + month: "numeric", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false, + weekday: "long", + }).format(d); +} + +function getTzOffsetMinutes(date: Date, timeZone: string) { + try { + const parts = new Intl.DateTimeFormat("en-US", { + timeZone, + timeZoneName: "shortOffset", + hour: "2-digit", + minute: "2-digit", + }).formatToParts(date); + + const tz = parts.find((p) => p.type === "timeZoneName")?.value ?? ""; + const m = tz.match(/([+-])(\d{1,2})(?::?(\d{2}))?/i); + if (!m) return -date.getTimezoneOffset(); + + const sign = m[1] === "-" ? -1 : 1; + const hh = Number(m[2] ?? 0); + const mm = Number(m[3] ?? 0); + return sign * (hh * 60 + mm); + } catch { + return -date.getTimezoneOffset(); + } +} + +function calculateHourDifferenceToViewer(now: Date) { + const viewerOffset = -now.getTimezoneOffset(); + const zagrebOffset = getTzOffsetMinutes(now, PROFILE.timezone); + const diffHours = Math.round((zagrebOffset - viewerOffset) / 60); + + if (diffHours < 0) return `(${String(Math.abs(diffHours))} hours behind you)`; + if (diffHours === 0) return "(Same time as you)"; + return `(${diffHours} hours ahead of you)`; +} + +function ReadonlyField({ label, value }: { label: string; value: string }) { + return ( +
+ + +
+ ); +} + +export default function Home() { + const [now, setNow] = useState(() => new Date()); + + useEffect(() => { + const t = setInterval(() => setNow(new Date()), 10_000); + return () => clearInterval(t); + }, []); + + const age = useMemo(() => calculateAge(BIRTHDATE_DMY), []); + const zagrebTime = getZagrebTimeString(now); + const tzDiff = calculateHourDifferenceToViewer(now); + + return ( +
+
+
+
{PROFILE.name}
+
+
+
+ +
+
+ Profile + +
+ profile picture + +
+ + +
+
+ + + + + +
+ + + + + + + +
+ +
+
+

+ Want to see code? Browse my{" "} + + GitHub + + . +

+
+ +
+

+ Looking for experience & project history? See my{" "} + + CV + {" "} + or{" "} + + LinkedIn + + . +

+
+
+
+ +
+ About + +
+ + +
+ +
+ +