diff --git a/.gitignore b/.gitignore
index c85fab7..16d54bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,27 +1,24 @@
-# Logs
-logs
-*.log
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
-lerna-debug.log*
-node_modules
-dist
-dist-ssr
-*.local
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
-# zone identifier files
-**/*Zone.Identifier
\ No newline at end of file
+# jetbrains setting folder
+.idea/
diff --git a/.prettierignore b/.prettierignore
index 1b8ac88..ab53970 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,3 +1,4 @@
# Ignore artifacts:
build
coverage
+dist
\ No newline at end of file
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..22a1505
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..d642209
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/README.md b/README.md
index 83c9a50..87b813a 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,43 @@
-# Portfolio
+# Astro Starter Kit: Minimal
-This repository houses my portfolio site.
+```sh
+npm create astro@latest -- --template minimal
+```
+
+> π§βπ **Seasoned astronaut?** Delete this file. Have fun!
+
+## π Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```text
+/
+βββ public/
+βββ src/
+β βββ pages/
+β βββ index.astro
+βββ package.json
+```
+
+Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
+
+There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
+
+Any static assets, like images, can be placed in the `public/` directory.
+
+## π§ Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :------------------------ | :----------------------------------------------- |
+| `npm install` | Installs dependencies |
+| `npm run dev` | Starts local dev server at `localhost:4321` |
+| `npm run build` | Build your production site to `./dist/` |
+| `npm run preview` | Preview your build locally, before deploying |
+| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro -- --help` | Get help using the Astro CLI |
+
+## π Want to learn more?
+
+Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
diff --git a/astro.config.mjs b/astro.config.mjs
new file mode 100644
index 0000000..f17737c
--- /dev/null
+++ b/astro.config.mjs
@@ -0,0 +1,9 @@
+// @ts-check
+import { defineConfig } from "astro/config";
+
+import react from "@astrojs/react";
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [react()],
+});
diff --git a/docs/wireframe.png b/docs/wireframe.png
deleted file mode 100644
index 621c852..0000000
Binary files a/docs/wireframe.png and /dev/null differ
diff --git a/eslint.config.js b/eslint.config.js
deleted file mode 100644
index 46c5909..0000000
--- a/eslint.config.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import js from "@eslint/js";
-import globals from "globals";
-import reactHooks from "eslint-plugin-react-hooks";
-import reactRefresh from "eslint-plugin-react-refresh";
-
-export default [
- { ignores: ["dist"] },
- {
- files: ["**/*.{js,jsx}"],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
- parserOptions: {
- ecmaVersion: "latest",
- ecmaFeatures: { jsx: true },
- sourceType: "module",
- },
- },
- plugins: {
- "react-hooks": reactHooks,
- "react-refresh": reactRefresh,
- },
- rules: {
- ...js.configs.recommended.rules,
- ...reactHooks.configs.recommended.rules,
- "no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
- "react-refresh/only-export-components": [
- "warn",
- { allowConstantExport: true },
- ],
- },
- },
-];
diff --git a/index.html b/index.html
deleted file mode 100644
index 4b4454c..0000000
--- a/index.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
- ${JSON.stringify(STRUCTURED_DATA)}`,
- }}
- />
- >
- );
-};
-
-const App = () => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default App;
diff --git a/src/components/ContactSection/ContactSection.module.css b/src/components/ContactSection/ContactSection.module.css
deleted file mode 100644
index a9b79e5..0000000
--- a/src/components/ContactSection/ContactSection.module.css
+++ /dev/null
@@ -1,79 +0,0 @@
-.section {
- position: relative;
- background-color: var(--mantine-color-dark-9);
-}
-
-.inner {
- composes: inner from global;
- max-width: 1000px !important;
- display: grid;
- place-items: center;
-}
-
-.largeText {
- font-size: var(--5xl);
- line-height: calc(var(--5xl) * 1.2);
- font-weight: 300;
- letter-spacing: 0.1rem;
- color: var(--mantine-color-gray-5);
- text-align: center;
- max-width: 50rem;
-}
-
-.headingTop {
- color: white;
-}
-
-.newLine {
- display: flex;
- align-items: center;
- width: 100%;
- justify-content: space-between;
- gap: 2rem;
-}
-
-/* decoration */
-.curve {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- overflow: hidden;
- line-height: 0;
- transform: rotate(180deg);
- z-index: 9;
-}
-
-.curve svg {
- position: relative;
- display: block;
- width: calc(106% + 1.2px);
- height: 150px;
- transform: rotateY(180deg);
-}
-
-.curve .shape-fill {
- fill: white;
-}
-
-/* MEDIA QUERIES */
-@media (max-width: $mantine-breakpoint-sm) {
- .largeText {
- font-size: var(--4xl);
- }
-
- .newLine {
- gap: 0.75rem;
- }
-}
-
-@media (max-width: $mantine-breakpoint-xs) {
- .newLine {
- flex-direction: column;
- gap: 1.5rem;
- }
-
- .largeText {
- display: none;
- }
-}
diff --git a/src/components/ContactSection/index.jsx b/src/components/ContactSection/index.jsx
deleted file mode 100644
index a80e699..0000000
--- a/src/components/ContactSection/index.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { Button, Divider, Title } from "@mantine/core";
-import { IconArrowRight } from "@tabler/icons-react";
-import styles from "./ContactSection.module.css";
-
-const ContactSection = () => {
- return (
-
- );
-};
-
-export default ContactSection;
diff --git a/src/components/ExperienceSection/ExperienceSection.module.css b/src/components/ExperienceSection/ExperienceSection.module.css
deleted file mode 100644
index 006f7d8..0000000
--- a/src/components/ExperienceSection/ExperienceSection.module.css
+++ /dev/null
@@ -1,128 +0,0 @@
-.section {
- background-color: black;
-}
-
-.inner {
- composes: inner from global;
- display: grid;
- grid-template-columns: 1fr 3fr;
- gap: 8rem;
-}
-
-.headingSectionKeyword {
- composes: sectionHeadingKeyword from global;
-}
-
-.heading {
- composes: sectionHeading from global;
- text-align: left;
-}
-
-.experiences {
- display: grid;
- grid-template-columns: 1fr 1fr;
- grid-template-rows: 1fr 1fr;
- gap: var(--5xl) var(--3xl);
-}
-
-.experience {
- border-left: 3px solid var(--mantine-color-gray-7);
- padding: 0 var(--mantine-spacing-lg);
- display: flex;
- flex-direction: column;
- gap: var(--mantine-spacing-xs);
-}
-
-.small {
- font-size: var(--small);
- color: var(--mantine-color-gray-7);
-}
-
-.employer {
- color: var(--mantine-color-gray-1);
- font-size: var(--3xl);
- font-weight: 500;
- margin: -0.5rem 0;
-}
-
-.jobTitle {
- color: var(--mantine-color-gray-4);
- font-weight: normal;
- font-size: var(--lg);
- text-wrap: nowrap;
-}
-
-.dates {
- composes: paragraphText from global;
- color: var(--mantine-color-dark-2) !important;
-}
-
-.paragraphText {
- composes: paragraphText from global;
- line-height: 1.4rem;
- color: var(--mantine-color-gray-5);
-}
-
-/* MEDIA QUERIES */
-@media (max-width: $mantine-breakpoint-lg) {
- .inner {
- gap: 6rem;
- }
-
- .experiences {
- gap: var(--5xl) var(--2xl);
- }
-}
-
-@media (max-width: 1078px) {
- .inner {
- display: flex;
- flex-direction: column;
- gap: 2rem;
- }
-
- .heading {
- text-align: center;
- }
-
- .headTextContainer {
- text-align: center;
- align-items: center !important;
- }
-}
-
-@media (max-width: $mantine-breakpoint-sm) {
- .experiences {
- grid-template-columns: 1fr;
- gap: var(--3xl);
- }
-
- .experience {
- gap: 0.25rem;
- }
-
- .employer {
- font-size: var(--2xl);
- margin: 0;
- }
-
- .dates,
- .small {
- font-size: var(--sm);
- }
-}
-
-@media (max-width: $mantine-breakpoint-xs) {
- .employer {
- font-size: var(--xl);
- }
-
- .jobTitle {
- font-size: 1rem;
- }
-
- .paragraphText {
- font-size: var(--mantine-font-size-sm);
- line-height: calc(var(--mantine-font-size-sm) * 1.4) !important;
- }
-}
diff --git a/src/components/ExperienceSection/index.jsx b/src/components/ExperienceSection/index.jsx
deleted file mode 100644
index 3c270c7..0000000
--- a/src/components/ExperienceSection/index.jsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Stack } from "@mantine/core";
-import experiences from "../../data/experiences";
-import styles from "./ExperienceSection.module.css";
-
-const ExperienceSection = () => (
-
-
-
- About
- Experience
-
-
-
- {experiences.map((e, idx) => (
-
-
- {String(idx + 1).padStart(2, "0")}.
-
- {e.employer}
- {e.title}
-
- {e.startDate} - {e.endDate}
-
- {e.description}
-
- ))}
-
-
-
-);
-
-export default ExperienceSection;
diff --git a/src/components/Footer/Footer.module.css b/src/components/Footer/Footer.module.css
deleted file mode 100644
index a470e26..0000000
--- a/src/components/Footer/Footer.module.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.footer {
- background-color: var(--mantine-color-dark-9);
- display: grid;
- place-items: center;
- margin-top: -2rem;
- padding-bottom: 2rem;
-}
-
-/* MEDIA QUERIES */
-@media (max-width: 589px) {
- .footer {
- margin-top: -1.5rem;
- padding-bottom: 1.5rem;
- }
-}
diff --git a/src/components/Footer/index.jsx b/src/components/Footer/index.jsx
deleted file mode 100644
index aefd398..0000000
--- a/src/components/Footer/index.jsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { useMediaQuery } from "@mantine/hooks";
-import { ActionIcon, Group } from "@mantine/core";
-import {
- IconBrandLinkedin,
- IconBrandGithub,
- IconMail,
-} from "@tabler/icons-react";
-import styles from "./Footer.module.css";
-
-const Footer = () => {
- const isNarrowScreen = useMediaQuery("(max-width: 589px)");
-
- const contactData = [
- {
- type: "Email",
- link: "mailto:hello@henrylin.io",
- icon:
,
- },
- {
- type: "GitHub",
- link: "https://github.com/henrylin03/",
- icon:
,
- },
- {
- type: "LinkedIn",
- link: "https://www.linkedin.com/in/henrylin03",
- icon:
,
- },
- ];
-
- return (
-
-
- {contactData.map((c, idx) => (
-
- {c.icon}
-
- ))}
-
-
- );
-};
-
-export default Footer;
diff --git a/src/components/Header/Header.module.css b/src/components/Header/Header.module.css
deleted file mode 100644
index 86c558b..0000000
--- a/src/components/Header/Header.module.css
+++ /dev/null
@@ -1,55 +0,0 @@
-.header {
- background-color: black;
- margin: 0 auto;
-}
-
-.inner {
- composes: inner from global;
- display: grid;
- grid-template-columns: 1fr 3.7fr 1fr;
- align-items: center;
-}
-
-.burger {
- justify-self: end;
-}
-
-/* branding */
-.logoIcon {
- width: 1rem;
-}
-
-.logoText {
- height: 1rem;
-}
-
-/* nav */
-.nav {
- place-self: center;
-}
-
-.navLink {
- color: var(--mantine-color-gray-4);
- text-decoration: none;
- padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
- font-size: var(--mantine-font-size-sm);
- border-radius: var(--mantine-radius-sm);
- transition: background-color 0.05s ease-out;
-
- @mixin hover {
- background-color: var(--mantine-color-dark-8);
- }
-}
-
-/* right-side */
-.ctaButton {
- composes: primaryBtn from global;
- justify-self: end;
-}
-
-/* MEDIA QUERIES */
-@media (max-width: $mantine-breakpoint-sm) {
- .navLink {
- padding: var(--mantine-spacing-xs);
- }
-}
diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx
deleted file mode 100644
index d27c8cb..0000000
--- a/src/components/Header/index.jsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { useDisclosure } from "@mantine/hooks";
-import { Group, Button, Burger, Drawer, Stack, Divider } from "@mantine/core";
-import logoIcon from "/branding/logoWhite.svg";
-import logoText from "/branding/textWhite.svg";
-import styles from "./Header.module.css";
-
-const LINKS_DATA = [
- { label: "Services", link: "#services" },
- { label: "Projects", link: "#projects" },
- { label: "About", link: "#about" },
- { label: "Contact", link: "#contact" },
-];
-
-const Header = () => {
- const [drawerOpened, { toggle: toggleDrawer, close: closeDrawer }] =
- useDisclosure(false);
-
- const navLinksArray = LINKS_DATA.map((l) => (
-
- {l.label}
-
- ));
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {navLinksArray}
-
-
-
-
- Let's chat
-
-
-
-
-
- {/* drawer */}
-
- {navLinksArray}
-
-
-
-
- Let's chat
-
-
-
- );
-};
-
-export default Header;
diff --git a/src/components/HeroSection/HeroSection.module.css b/src/components/HeroSection/HeroSection.module.css
deleted file mode 100644
index cfd5e35..0000000
--- a/src/components/HeroSection/HeroSection.module.css
+++ /dev/null
@@ -1,211 +0,0 @@
-.hero {
- position: relative;
- overflow: hidden;
- padding-bottom: 8rem;
-}
-
-.inner {
- composes: inner from global;
- display: flex;
- flex-direction: column;
- gap: 2rem;
-}
-
-/* SUB-SECTIONS */
-.top {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.bottom {
- display: grid;
- grid-template-columns: 1fr 1fr;
-}
-
-/* TEXT */
-.greeting {
- font-size: var(--xl);
- color: var(--mantine-color-gray-3);
- text-wrap: no-wrap;
- opacity: 0;
- transform: translateY(1rem);
- animation: fadeInUp 0.6s ease-out forwards;
-}
-
-.heading {
- font-size: var(--5xl);
- color: white;
- opacity: 0;
- transform: translateY(1rem);
- animation: fadeInUp 1.8s ease-out 0.5s forwards;
-}
-
-.text {
- composes: paragraphText from global;
- text-align: right;
- opacity: 0;
- transform: translateY(1rem);
- animation: fadeInUp 2s ease-out 1.5s forwards;
-}
-
-/* BUTTON */
-.ctaBtn {
- composes: primaryBtn from global;
- justify-self: start;
- opacity: 0;
- transform: translateY(1rem);
- animation: fadeInUp 2s ease-out 1.5s forwards;
-}
-
-.ctaBtn:hover {
- transform: scale(1.05) !important;
- transition: transform 0.3s ease;
-}
-
-/* DECORATION */
-.container {
- background-color: black;
- height: 100%;
- width: 100dvw;
- overflow: hidden;
- position: absolute;
- top: 0;
- z-index: -1;
-}
-
-.logoPattern {
- height: 100%;
- width: 100%;
- background-image: url("/images/background.png");
- background-size: 65%;
- background-position: center top;
- background-repeat: repeat;
- position: absolute;
- left: 50%;
- top: 0;
- transform: translate(-50%, 0%);
- z-index: -1;
- will-change: background-position;
- animation: panBackground 30s linear infinite;
-}
-
-.gradientOverlay {
- background: radial-gradient(circle, transparent 10%, black);
- width: 100%;
- height: 100%;
- position: absolute;
- left: 0;
- top: 0;
- z-index: 1;
- animation: pulseOverlay 6s ease-in-out infinite;
-}
-
-/* ANIMATIONS */
-@keyframes fadeInUp {
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes panBackground {
- 0% {
- background-position: center 0%;
- }
- 100% {
- background-position: center 200%;
- }
-}
-
-@keyframes panBackgroundMobile {
- 0% {
- background-position: center 0%;
- }
- 100% {
- background-position: center 100%; /* Shorter panning distance */
- }
-}
-
-@keyframes pulseOverlay {
- 0%,
- 100% {
- opacity: 0.5;
- }
- 50% {
- opacity: 1;
- }
-}
-
-/* MEDIA QUERIES */
-@media (max-width: 1024px) {
- .hero {
- padding: 5rem 0;
- }
-
- .logoPattern {
- background-size: cover;
- animation: panBackgroundMobile 15s linear infinite; /* Shorter panning for mobile */
- }
-
- .gradientOverlay {
- animation: pulseOverlayMobile 8s ease-in-out infinite; /* Slower pulse for mobile */
- }
-}
-
-@media (max-width: $mantine-breakpoint-lg) {
- .hero {
- padding: 5rem 0;
- }
-
- .logoPattern {
- background-size: cover;
- }
-}
-
-@media (max-width: 817px) {
- .greeting {
- font-size: var(--lg);
- }
-
- .heading {
- font-size: var(--4xl);
- }
-}
-
-@media (max-width: $mantine-breakpoint-md) {
- .top {
- gap: 0.25rem;
- }
-}
-
-@media (max-width: $mantine-breakpoint-sm) {
- .heading {
- line-height: calc(3.25 * var(--lg));
- }
-}
-
-@media (max-width: $mantine-breakpoint-xs) {
- .hero {
- padding: 4rem 0;
- }
-
- .inner,
- .bottom {
- gap: 1.5rem;
- }
-
- .bottom {
- grid-template-columns: 1fr;
- }
-
- .greeting {
- margin-bottom: 0.5rem;
- }
-
- .text {
- order: -1;
- text-align: left;
- max-width: 90%;
- }
-}
diff --git a/src/components/HeroSection/index.jsx b/src/components/HeroSection/index.jsx
deleted file mode 100644
index 8738607..0000000
--- a/src/components/HeroSection/index.jsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { Button } from "@mantine/core";
-import { IconArrowRight } from "@tabler/icons-react";
-import styles from "./HeroSection.module.css";
-
-const HeroSection = () => (
-
-
-
-
Hi, I'm Henry.
-
Web Developer.
-
-
-
}
- className={styles.ctaBtn}
- component="a"
- href="#contact"
- >
- Let's chat
-
-
- I design and build custom, pixel-perfect web apps that my users love
- to use.
-
-
-
-
- {/* decoration */}
-
-
-);
-
-export default HeroSection;
diff --git a/src/components/ProjectsSection/ProjectsSection.module.css b/src/components/ProjectsSection/ProjectsSection.module.css
deleted file mode 100644
index 118a9c3..0000000
--- a/src/components/ProjectsSection/ProjectsSection.module.css
+++ /dev/null
@@ -1,161 +0,0 @@
-.section {
- background: linear-gradient(to bottom, black, var(--mantine-color-dark-9));
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
-}
-
-.inner {
- composes: inner from global;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 3rem;
-}
-
-.projectCards {
- gap: 3rem !important;
-}
-
-.card {
- composes: transparentCard from global;
- display: grid;
- grid-template-columns: 1fr 1.25fr;
- gap: 1.5rem;
-}
-
-.left {
- display: flex;
- flex-direction: column;
- gap: var(--mantine-spacing-xs);
- align-self: start;
-}
-
-.small {
- font-size: var(--sm);
- color: var(--mantine-color-gray-6);
- margin-bottom: calc(-1 * var(--mantine-spacing-xs));
-}
-
-.projectName {
- color: white;
- font-size: var(--3xl);
-}
-
-.paragraphText {
- composes: paragraphText from global;
-}
-
-.calloutList {
- margin-top: var(--mantine-spacing-xs) !important;
-}
-
-.screenshotAnchor {
- overflow: hidden;
- border-radius: var(--mantine-radius-md);
-}
-
-.screenshot {
- height: 100%;
- width: 100% !important;
- min-width: 100%;
- max-width: 100%;
- object-fit: cover !important;
- object-position: top left;
- filter: grayscale(50%);
- -webkit-filter: grayscale(50%);
- transition: filter 0.2s ease-out;
- transition: transform 0.2s ease-out;
-
- @mixin hover {
- filter: none;
- -webkit-filter: grayscale(0);
- transform: scale(1.05) translate(2%, 2%);
- }
-}
-
-.buttons {
- display: flex;
- align-items: center;
- gap: var(--mantine-spacing-sm);
-}
-
-.viewMoreOnGitHubButton {
- composes: secondaryBtn from global;
- margin-top: var(--mantine-spacing-md);
-}
-
-/* MEDIA QUERIES */
-/* cards are single column */
-@media (max-width: $mantine-breakpoint-lg) {
- .card {
- grid-template-columns: 1fr;
- gap: var(--mantine-spacing-xl);
- }
-
- .small,
- .projectName,
- .paragraphText {
- text-align: center;
- }
-
- .buttons {
- justify-content: center;
- }
-
- .screenshotAnchor {
- order: -1;
- }
-
- .calloutList {
- place-self: center !important;
- max-width: 480px;
- padding: 0 var(--mantine-spacing-xl) !important;
- }
-}
-
-@media (max-width: $mantine-breakpoint-md) {
- .projectCards {
- gap: var(--mantine-spacing-xl) !important;
- }
-
- .card {
- grid-template-rows: 1fr fit-content;
- }
-}
-
-@media (max-width: $mantine-breakpoint-sm) {
- .screenshot {
- filter: none;
- -webkit-filter: grayscale(0);
- }
-}
-
-@media (max-width: $mantine-breakpoint-xs) {
- .inner {
- gap: var(--mantine-spacing-xl);
- }
-
- .small {
- font-size: var(--xs);
- }
-
- .projectName {
- font-size: var(--2xl);
- }
-
- .calloutList {
- width: 100%;
- padding: 0 var(--mantine-spacing-xs) !important;
- }
-
- .card {
- gap: var(--mantine-spacing-lg);
- }
-
- .buttons > * {
- flex: 1;
- }
-
- .viewMoreOnGitHubButton {
- margin-top: var(--mantine-spacing-xs);
- }
-}
diff --git a/src/components/ProjectsSection/index.jsx b/src/components/ProjectsSection/index.jsx
deleted file mode 100644
index f33b4bd..0000000
--- a/src/components/ProjectsSection/index.jsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import { Button, Image, List, Stack, ThemeIcon } from "@mantine/core";
-import { IconCheck } from "@tabler/icons-react";
-import projectsData from "../../data/projects";
-import styles from "./ProjectsSection.module.css";
-import { useMediaQuery } from "@mantine/hooks";
-
-const ProjectsSection = () => {
- const isNarrowScreen = useMediaQuery("(max-width: 499px)");
-
- const cards = projectsData.map((project) => (
-
-
-
{project.type}
-
- {project.title}
-
-
{project.copy}
-
-
-
- }
- >
- {project.callouts.map((callout, idx) => (
- {callout}
- ))}
-
-
- {project.buttons.map((btn, idx) => (
-
- {btn[0]}
-
- ))}
-
-
-
-
-
-
-
- ));
-
- return (
-
-
-
- Projects
- My recent work
-
-
-
- {cards}
-
-
-
- View more on GitHub
-
-
-
- );
-};
-
-export default ProjectsSection;
diff --git a/src/components/ServicesSection/ServicesSection.module.css b/src/components/ServicesSection/ServicesSection.module.css
deleted file mode 100644
index a072b52..0000000
--- a/src/components/ServicesSection/ServicesSection.module.css
+++ /dev/null
@@ -1,121 +0,0 @@
-.services {
- background: linear-gradient(to bottom, black, var(--mantine-color-dark-9));
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
-}
-
-.services svg {
- height: 100%;
- width: 100%;
-}
-
-.inner {
- composes: inner from global;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: var(--mantine-spacing-xl);
-}
-
-.grid {
- margin-top: 3vmin;
-
- /* grid */
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 4vmin;
-}
-
-.card {
- padding: var(--mantine-spacing-xl);
- background-color: var(--mantine-color-dark-8);
- border-radius: var(--mantine-radius-xl);
- border: 0.25rem solid var(--mantine-color-dark-4);
-
- display: flex;
- flex-direction: column;
- gap: var(--mantine-spacing-sm);
-}
-
-.cardIcon {
- height: var(--3xl);
- width: var(--3xl);
- color: var(--mantine-color-dark-3);
-}
-
-.top {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: var(--mantine-spacing-sm);
-}
-
-.cardHeading {
- font-size: var(--3xl);
- line-height: calc(var(--3xl) * 1.3);
- color: white;
-}
-
-.skillsContainer {
- margin-top: var(--mantine-spacing-md);
- list-style: none;
- display: flex;
- align-items: center;
- gap: var(--mantine-spacing-xl);
-}
-
-.skill {
- color: var(--mantine-color-dark-1);
-
- /* flexbox */
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.25rem;
-}
-
-.skillIcon {
- height: var(--2xl);
- width: var(--2xl);
-}
-
-.skillLabel {
- font-size: var(--xs);
- text-wrap: nowrap;
-}
-
-/* MEDIA QUERIES */
-@media (max-width: $mantine-breakpoint-md) {
- .grid {
- gap: 2vmin;
- margin-top: 1rem;
- }
-
- .cardIcon {
- width: var(--2xl);
- height: var(--2xl);
- }
-}
-
-@media (max-width: $mantine-breakpoint-sm) {
- .grid {
- grid-template-columns: 1fr;
- gap: var(--mantine-spacing-xl);
- }
-
- .card {
- padding: 5vmin;
- }
-}
-
-@media (max-width: 350px) {
- .skillsContainer {
- gap: var(--mantine-spacing-lg);
- }
-}
-
-@media (max-width: $mantine-breakpoint-xs) {
- .cardHeading {
- font-size: var(--2xl);
- line-height: calc(var(--2xl) * 1.3);
- }
-}
diff --git a/src/components/ServicesSection/index.jsx b/src/components/ServicesSection/index.jsx
deleted file mode 100644
index 3a2a151..0000000
--- a/src/components/ServicesSection/index.jsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { Stack } from "@mantine/core";
-import {
- IconCode,
- IconPalette,
- IconBrandReact,
- IconLayoutBoard,
- IconSearch,
- IconTestPipe,
-} from "@tabler/icons-react";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faHtml5, faCss3Alt, faJs } from "@fortawesome/free-brands-svg-icons";
-import styles from "./ServicesSection.module.css";
-
-const SERVICES_DATA = [
- {
- icon:
,
- title: "Web Development",
- text: "I code websites and web applications from scratch, helping bring your vision to life in the browser.",
- skills: [
- { label: "HTML", icon:
},
- { label: "CSS", icon:
},
- { label: "JavaScript", icon:
},
- { label: "React.js", icon:
},
- ],
- },
-
- {
- icon:
,
- title: "UI/UX Design",
- text: "I conduct user research to understand your users' needs, solving their pain points, and testing potential solutions with them.",
- skills: [
- { label: "Wireframing", icon:
},
- { label: "UX research", icon:
},
- { label: "User testing", icon:
},
- ],
- },
-];
-
-const ServicesSection = () => (
-
-
- {/* Text */}
-
- Services
- Here to help
-
- Whether you're starting from scratch or have an existing website or
- web app needing a revamp,{" "}
-
- let's have a chat
- {" "}
- on how I can help!
-
-
-
- {/* Cards */}
-
- {SERVICES_DATA.map((service) => (
-
-
- {service.icon}
-
-
-
{service.title}
-
{service.text}
-
-
- {service.skills.map((skill) => (
-
-
- {skill.icon}
-
- {skill.label}
-
- ))}
-
-
- ))}
-
-
-
-);
-
-export default ServicesSection;
diff --git a/src/components/TestimonialsSection/TestimonialsCarousel.jsx b/src/components/TestimonialsSection/TestimonialsCarousel.jsx
deleted file mode 100644
index 686f74d..0000000
--- a/src/components/TestimonialsSection/TestimonialsCarousel.jsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { useRef } from "react";
-import { useMediaQuery } from "@mantine/hooks";
-import Autoplay from "embla-carousel-autoplay";
-import { Carousel } from "@mantine/carousel";
-import testimonials from "../../data/testimonials";
-import styles from "./TestimonialsSection.module.css";
-
-const TestimonialsCarousel = () => {
- const isNarrowScreen = useMediaQuery("(max-width: 449px)");
- const autoplay = useRef(Autoplay({ delay: 12000 }));
-
- const slides = testimonials.map((testimonial) => (
-
-
- "{testimonial.fullText}"
-
-
-
-
-
- {testimonial.author.name}
-
-
- {testimonial.author.title}, {testimonial.clientName}
-
-
-
-
-
- ));
-
- return (
-
- {slides}
-
- );
-};
-
-export default TestimonialsCarousel;
diff --git a/src/components/TestimonialsSection/TestimonialsCarousel.test.jsx b/src/components/TestimonialsSection/TestimonialsCarousel.test.jsx
deleted file mode 100644
index cfc0b2b..0000000
--- a/src/components/TestimonialsSection/TestimonialsCarousel.test.jsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { render, screen } from "../../../test-utils";
-import testimonials from "../../data/testimonials";
-import TestimonialsCarousel from "./TestimonialsCarousel";
-
-describe("TestimonialsCarousel", () => {
- it("renders carousel with correct number of testimonials", () => {
- render(
);
- const slides = screen.getAllByRole("group");
- expect(slides).toHaveLength(testimonials.length);
- });
-
- it("displays testimonial content correctly", () => {
- render(
);
-
- // test using the first 2 testimonials
- for (let i = 0; i < 2; i++) {
- const testimonial = testimonials[i];
-
- expect(screen.getByText(`"${testimonial.fullText}"`)).toBeInTheDocument();
- expect(screen.getByText(testimonial.author.name)).toBeInTheDocument();
- expect(
- screen.getByText(
- `${testimonial.author.title}, ${testimonial.clientName}`,
- ),
- ).toBeInTheDocument();
- expect(
- screen.getByAltText(`Photo of ${testimonial.author.name}`),
- ).toBeInTheDocument();
- }
- });
-});
diff --git a/src/components/TestimonialsSection/TestimonialsSection.module.css b/src/components/TestimonialsSection/TestimonialsSection.module.css
deleted file mode 100644
index 883da90..0000000
--- a/src/components/TestimonialsSection/TestimonialsSection.module.css
+++ /dev/null
@@ -1,90 +0,0 @@
-.section {
- background-color: black;
-}
-
-.card {
- composes: transparentCard from global;
- border-radius: var(--mantine-radius-lg) !important;
- border-width: 1px !important;
- height: 100%;
-
- display: grid;
- grid-template-columns: 1fr;
- grid-template-rows: fit-content 1fr;
- gap: var(--mantine-spacing-xl);
-}
-
-.testimonialText {
- font-size: var(--lg);
- align-self: end;
-}
-
-.authorContainer {
- align-self: end;
- display: flex;
- flex-direction: column;
- gap: var(--mantine-spacing-xs);
-}
-
-.authorImage {
- width: 5rem;
- height: 5rem;
- border-radius: 50%;
-}
-
-.authorText {
- display: flex;
- flex-direction: column;
- gap: 0.3rem;
-}
-
-.authorDetails {
- margin-top: -0.25rem;
- color: var(--mantine-color-gray-6);
-}
-
-/* MEDIA QUERIES */
-@media (max-width: $mantine-breakpoint-md) {
- .card {
- padding: 3.5rem !important;
- }
-}
-
-@media (max-width: $mantine-breakpoint-sm) {
- .card {
- padding: var(--mantine-spacing-xl) !important;
- }
-
- .testimonialText {
- font-size: 1rem;
- }
-
- .authorText {
- font-size: var(--mantine-font-size-sm);
- }
-
- .authorImage {
- width: 4rem;
- height: 4rem;
- }
-}
-
-@media (max-width: $mantine-breakpoint-xs) {
- .authorText {
- font-size: var(--mantine-font-size-xs);
- }
-
- .authorText {
- gap: 0.15rem;
- }
-}
-
-@media (max-width: 459px) {
- .testimonialText {
- font-size: var(--mantine-font-size-sm);
- }
-
- .authorText {
- font-size: var(--mantine-font-size-xs);
- }
-}
diff --git a/src/components/TestimonialsSection/index.jsx b/src/components/TestimonialsSection/index.jsx
deleted file mode 100644
index 6aea749..0000000
--- a/src/components/TestimonialsSection/index.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Stack } from "@mantine/core";
-import TestimonialsCarousel from "./TestimonialsCarousel";
-import styles from "./TestimonialsSection.module.css";
-
-const TestimonialsSection = () => (
-
-
-
- Testimonials
-
- What my clients say
-
-
-
-
-
-
-);
-
-export default TestimonialsSection;
diff --git a/src/data/experiences.js b/src/data/experiences.js
deleted file mode 100644
index 7a45ffc..0000000
--- a/src/data/experiences.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const experiences = [
- {
- employer: "LEAP",
- title: "Frontend Web Developer",
- startDate: "May 2025",
- endDate: "Present",
- description:
- "I build and maintain web apps used by lawyers in their legal practice management software.",
- },
-
- {
- employer: "Henry Lin",
- title: "Freelance Web Developer",
- startDate: "Jul 2023",
- endDate: "Present",
- description:
- "I designed, built and maintained custom websites and web applications.",
- },
-
- {
- employer: "CommBank",
- title: "Automation Engineer / UX Designer",
- startDate: "Aug 2021",
- endDate: "Jul 2024",
- description:
- "Designed web apps and developed Python automation scripts to streamline cyber compliance processes.",
- },
-
- {
- employer: "Telstra",
- title: "Legal Automation Engineer",
- startDate: "Aug 2020",
- endDate: "Aug 2021",
- description:
- "Designed and built web apps that saved 6,000+ hours per year for the legal team.",
- },
-];
-
-export default experiences;
diff --git a/src/data/projects.js b/src/data/projects.js
deleted file mode 100644
index 834df2e..0000000
--- a/src/data/projects.js
+++ /dev/null
@@ -1,85 +0,0 @@
-const projects = [
- {
- id: "eqa-website",
- type: "Local Business Website",
- title: "Equinox Academy",
- copy: "I redesigned and built the website of a local martial arts gym in React.js",
- callouts: [
- "Improving page load speeds by 40%,",
- "Increased organic search traffic by 130% through SEO.",
- ],
- buttons: [["Visit website", "https://equinoxacademy.com.au"]],
- imgPaths: [
- "/images/projects/eqa-website.png",
- "/images/projects/eqa-website-tablet.png",
- "/images/projects/eqa-website-mobile.png",
- ],
- testimonialId: "eqa1",
- technologies: [
- "React.js",
- "EmailJS",
- "React Router",
- "Vitest",
- "React Testing Library",
- "Vite",
- ],
- },
-
- {
- id: "le-vesinet",
- type: "E-Commerce Web App",
- title: "Le Vesinet",
- copy: "I rebuilt an e-commerce Shopify platform using React.js:",
- callouts: [
- "Cutting page load times by 80%+ using client-side routing,",
- "Automated tests to ensure code reliability.",
- ],
- imgPaths: [
- "/images/projects/le-vesinet.png",
- "/images/projects/le-vesinet-tablet.png",
- "/images/projects/le-vesinet-mobile.png",
- ],
- buttons: [
- ["Visit website", "https://le-vesinet.netlify.app/"],
- ["View code", "https://github.com/henrylin03/le-vesinet"],
- ],
- technologies: [
- "React.js",
- "React Router",
- "Vitest",
- "React Testing Library",
- "Vite",
- ],
- },
-
- {
- id: "pokemems",
- type: "Web Game",
- title: "PokΓ©mems",
- copy: "I designed and built a React.js memory game.",
- callouts: [
- "Built custom hook for async fetching from PokΓ©API, reducing API response times by 60%+,",
- "Optimised algorithms to speed up game state updates by 50%+,",
- "Implemented automated unit, integration and end-to-end testing.",
- ],
- imgPaths: [
- "/images/projects/pokemems.png",
- "/images/projects/pokemems-tablet.png",
- "/images/projects/pokemems-mobile.png",
- ],
- buttons: [
- ["Play game", "https://poke-mems.netlify.app/"],
- ["View code", "https://github.com/henrylin03/pokemems"],
- ],
- technologies: [
- "React.js",
- "Vite",
- "Vitest",
- "React Testing Library",
- "Cypress",
- "API",
- ],
- },
-];
-
-export default projects;
diff --git a/src/data/testimonials.js b/src/data/testimonials.js
deleted file mode 100644
index cddde42..0000000
--- a/src/data/testimonials.js
+++ /dev/null
@@ -1,42 +0,0 @@
-const testimonials = [
- {
- id: "eqa1",
- projectId: "eqa-website",
- clientName: "Equinox Academy",
- author: {
- name: "James Chen & Kobi Gregory",
- title: "Directors",
- img: "/images/testimonialProfiles/eqa1.jpg",
- },
- fullText:
- "Henry delivered phenomenal service in developing our martial arts club's website. With his extensive martial arts experience, he truly understood our vision and goals, creating a site that perfectly reflects our club's spirit and values. His responsiveness and attention to detail ensured a seamless process from start to finish. I highly recommend Henry for anyone seeking a professional and effective online presence tailored to their specific needs.",
- },
-
- {
- id: "intellex1",
- projectId: "intellex",
- clientName: "Intellex",
- author: {
- name: "Jason Ye",
- title: "Co-Founder",
- img: "/images/testimonialProfiles/intellex1-jason.jpg",
- },
- fullText:
- "Henry provided fantastic advice to us throughout the development of our website. He was knowledgeable and really focused on understanding what we were trying to achieve first. Would highly recommend his services if you are looking to build a user friendly website.",
- },
-
- {
- id: "intellex2",
- projectId: "intellex",
- clientName: "Intellex",
- author: {
- name: "Nicole Foo",
- title: "UI/UX Designer",
- img: "/images/testimonialProfiles/intellex2-nicole.jpg",
- },
- fullText:
- "Henry has been invaluable to our small team! He has provided guidance in many areas from project management, stakeholder comms, to product/UX thinking and technical expertise/advice when working with offshore developers. His work on the launch page reflects his breadth of skills across product thinking, meticulousness and technicality. He consistently pushes and ensures quality outcomes and business decisions. It's been reassuring to work with him π",
- },
-];
-
-export default testimonials;
diff --git a/src/main.jsx b/src/main.jsx
deleted file mode 100644
index 637e43b..0000000
--- a/src/main.jsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { StrictMode } from "react";
-import { createRoot } from "react-dom/client";
-import App from "./App.jsx";
-
-createRoot(document.getElementById("root")).render(
-
-
- ,
-);
diff --git a/src/pages/index.astro b/src/pages/index.astro
new file mode 100644
index 0000000..2d14107
--- /dev/null
+++ b/src/pages/index.astro
@@ -0,0 +1,16 @@
+---
+
+---
+
+
+
+
+
+
+
+
Astro
+
+
+
Astro
+
+
diff --git a/src/styles/global.css b/src/styles/global.css
deleted file mode 100644
index 1c888f4..0000000
--- a/src/styles/global.css
+++ /dev/null
@@ -1,149 +0,0 @@
-@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap");
-
-:root {
- scroll-behavior: smooth;
-
- /* font-sizes */
- --5xl: 3.815rem;
- --4xl: 3.052rem;
- --3xl: 2.441rem;
- --2xl: 1.953rem;
- --xl: 1.563rem;
- --lg: 1.25rem;
- --sm: 0.8rem;
- --xs: 0.64rem;
-}
-
-/* reset */
-* {
- padding: 0;
- margin: 0;
- box-sizing: border-box;
-}
-
-/* sections/wrappers */
-section {
- padding: 4rem 0;
-}
-
-.inner {
- max-width: 1200px;
- margin: 0 auto;
- padding: var(--mantine-spacing-md) var(--mantine-spacing-xl);
-}
-
-.sectionHeadTextContainer {
- text-align: center;
-}
-
-/* typography */
-h1,
-h2,
-h3 {
- text-wrap: balance;
- font-weight: 500;
-}
-
-p,
-li {
- text-wrap: pretty;
-}
-
-.sectionHeading {
- font-size: var(--4xl);
- line-height: calc(var(--4xl) * 1.3);
- color: white;
- text-align: center;
-}
-
-.paragraphText {
- line-height: 1.7rem;
- color: var(--mantine-color-gray-5);
-}
-
-.sectionHeadTextContainer > .paragraphText {
- margin: 0 auto;
- max-width: 75%;
-}
-
-.link {
- font-weight: 700;
- color: white;
- text-underline-offset: 0.5rem;
- text-decoration-color: transparent;
- transition: all 0.1s ease-out;
-
- @mixin hover {
- text-decoration-color: inherit;
- }
-}
-
-.sectionHeadingKeyword {
- font-size: var(--sm);
- color: var(--mantine-color-gray-6);
- font-weight: 500;
- margin-bottom: -1rem;
-}
-
-/* UI elements */
-.primaryBtn {
- transition: background-color 0.2s ease-out;
- @mixin hover {
- background-color: var(--mantine-color-gray-4) !important;
- }
-}
-
-.secondaryBtn {
- border-width: 2px;
- transition: background-color 0.2s ease-out;
- @mixin hover {
- background-color: var(--mantine-color-dark-7) !important;
- }
-}
-
-.transparentCard {
- padding: 3rem;
- border: 0.25rem solid var(--mantine-color-dark-4);
- border-radius: var(--mantine-radius-xl);
-}
-
-/* MEDIA QUERIES */
-@media (max-width: $mantine-breakpoint-md) {
- .transparentCard {
- padding: var(--mantine-spacing-xl);
- }
-}
-
-@media (max-width: $mantine-breakpoint-sm) {
- .sectionHeading {
- font-size: var(--3xl);
- line-height: calc(var(--3xl) * 1.3);
- }
-
- .sectionHeadTextContainer > .paragraphText {
- max-width: 85%;
- }
-}
-
-@media (max-width: $mantine-breakpoint-xs) {
- section {
- padding: 2rem 0;
- }
-
- .inner {
- padding: var(--mantine-spacing-md) var(--mantine-spacing-lg);
- }
-
- .sectionHeadTextContainer > .paragraphText {
- max-width: 100%;
- }
-
- .paragraphText {
- font-size: var(--mantine-font-size-sm);
- line-height: calc(var(--mantine-font-size-sm) * 1.5);
- }
-
- .transparentCard {
- padding: var(--mantine-spacing-md);
- }
-}
diff --git a/src/styles/theme.js b/src/styles/theme.js
deleted file mode 100644
index c2d24c3..0000000
--- a/src/styles/theme.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createTheme } from "@mantine/core";
-
-const theme = createTheme({
- fontFamily: "DM Sans, sans-serif",
- defaultRadius: "xl",
-});
-
-export default theme;
diff --git a/test-utils/index.js b/test-utils/index.js
deleted file mode 100644
index 9d9c65d..0000000
--- a/test-utils/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import userEvent from "@testing-library/user-event";
-
-export * from "@testing-library/react";
-export { render } from "./render";
-export { userEvent };
diff --git a/test-utils/render.tsx b/test-utils/render.tsx
deleted file mode 100644
index ee8589c..0000000
--- a/test-utils/render.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { render as testingLibraryRender } from "@testing-library/react";
-import { MantineProvider } from "@mantine/core";
-import theme from "../src/styles/theme";
-
-export function render(ui: React.ReactNode) {
- return testingLibraryRender(ui, {
- wrapper: ({ children }: { children: React.ReactNode }) => (
-
{children}
- ),
- });
-}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..5da5183
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "astro/tsconfigs/strictest",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react"
+ }
+}
diff --git a/vite.config.js b/vite.config.js
deleted file mode 100644
index 4849761..0000000
--- a/vite.config.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineConfig } from "vite";
-import react from "@vitejs/plugin-react";
-
-// https://vite.dev/config/
-export default defineConfig({
- plugins: [react()],
- test: {
- globals: true,
- environment: "jsdom",
- setupFiles: "./vitest.setup.mjs",
- },
-});
diff --git a/vitest.setup.mjs b/vitest.setup.mjs
deleted file mode 100644
index 047fa59..0000000
--- a/vitest.setup.mjs
+++ /dev/null
@@ -1,61 +0,0 @@
-import "@testing-library/jest-dom/vitest";
-
-import { vi } from "vitest";
-
-const { getComputedStyle } = window;
-window.getComputedStyle = (elt) => getComputedStyle(elt);
-window.HTMLElement.prototype.scrollIntoView = () => {};
-
-Object.defineProperty(window, "matchMedia", {
- writable: true,
- value: vi.fn().mockImplementation((query) => ({
- matches: false,
- media: query,
- onchange: null,
- addListener: vi.fn(),
- removeListener: vi.fn(),
- addEventListener: vi.fn(),
- removeEventListener: vi.fn(),
- dispatchEvent: vi.fn(),
- })),
-});
-
-class ResizeObserver {
- observe() {}
- unobserve() {}
- disconnect() {}
-}
-
-window.ResizeObserver = ResizeObserver;
-
-class IntersectionObserver {
- constructor(callback, options = {}) {
- this.callback = callback;
- this.options = options;
- this.entries = [];
- }
-
- observe(element) {
- const entry = {
- target: element,
- isIntersecting: true,
- intersectionRatio: 1,
- boundingClientRect: element.getBoundingClientRect(),
- rootBounds: null,
- time: Date.now(),
- };
- this.entries.push(entry);
-
- setTimeout(() => this.callback([entry], this), 0);
- }
-
- unobserve(element) {
- this.entries = this.entries.filter((entry) => entry.target !== element);
- }
-
- disconnect() {
- this.entries = [];
- }
-}
-
-window.IntersectionObserver = IntersectionObserver;