Changes include:
- Roboto Flex loaded from Google Fonts for compact UI text - Reduced decorative borders while retaining useful input and divider borders - Fixed blank journal detail pages - Password-protected journal publisher with four-hour signed sessions - New articles saved directly to posts.json - Prominent Spotify, Instagram, and Steam buttons on Contact - Desktop and mobile layouts visually verified - Production build and authorization tests passed
This commit is contained in:
+3
-1
@@ -8,6 +8,9 @@
|
||||
content="Alex Herlan is a senior software engineer building full-stack products, applied AI, and dependable cloud systems."
|
||||
/>
|
||||
<meta name="theme-color" content="#f7f8f4" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,100..1000&display=swap" rel="stylesheet" />
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2230%22 fill=%22%232f5147%22/><text x=%2250%22 y=%2264%22 text-anchor=%22middle%22 font-size=%2244%22 fill=%22white%22 font-family=%22Arial%22>AH</text></svg>" />
|
||||
<title>Alex Herlan — Software Engineer</title>
|
||||
</head>
|
||||
@@ -16,4 +19,3 @@
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Generated
+689
@@ -8,6 +8,10 @@
|
||||
"name": "alex-herlan-portfolio",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@tiptap/extension-text-style": "^3.31.3",
|
||||
"@tiptap/pm": "^3.31.3",
|
||||
"@tiptap/react": "^3.31.3",
|
||||
"@tiptap/starter-kit": "^3.31.3",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-router-dom": "^7.18.3"
|
||||
@@ -18,6 +22,34 @@
|
||||
"vite": "^8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
|
||||
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
|
||||
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.8.0",
|
||||
"@floating-ui/utils": "^0.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
|
||||
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.149.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz",
|
||||
@@ -308,6 +340,475 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.31.3.tgz",
|
||||
"integrity": "sha512-Cz50pvciQrxdSxgTkHOVz0uD0Yl/8Xt0QatGD6ILm47jW8EzyHR9RkUGs/D5IqzXKuVPntfw1ttaT926vXfiRg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/pm": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-blockquote": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.31.3.tgz",
|
||||
"integrity": "sha512-fyY2XMbyDDDfOTQ1Qdrnqa1qwC9DWE4n7AfE0EKQI0G8MfLV8RaDlLDcOZDJ9JbMPY7/Gx7EjyK4NKxSW9n2hQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bold": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.31.3.tgz",
|
||||
"integrity": "sha512-dIuYhKk8TitKU/FeDpoTeWZhU42YgDN5npgWNjAmMmRktPdoxnH3/wGSiwXlqZgJWjehN7kPWDePWpJeAImGpQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bubble-menu": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.31.3.tgz",
|
||||
"integrity": "sha512-EV6ZnwKc++2OM/OcD54n8s1B7C9LP7GKAtdEPwu0t3BYJf3sG8Ueoinf7SgGPO9oMPg96GneOkDNm1urMV167g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bullet-list": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.31.3.tgz",
|
||||
"integrity": "sha512-qEyyoPapPef4LO8XKaN83bxtaNzkJ4kFn/IxLnEKd4BJ3Mvi4MH2yJYlyDqwFnMoev4pMA1zBHRAt/C0THRmZw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.31.3.tgz",
|
||||
"integrity": "sha512-SzxOqchrD2AcN3uT67PjKmRFEMOU3vNNiwNaamZJUbZrI6Hmy+bvdHJrc3jrIddyyCrAtsnAfRI0cmorW1jGfg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code-block": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.31.3.tgz",
|
||||
"integrity": "sha512-nvknt4FhyJQjYcvxptmeUlFsIAc8ibua3E5BN4Pim374/9RWepH4cdE9X0/qUTtguHElLx0iKtSIY3rW2qGTFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-document": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.31.3.tgz",
|
||||
"integrity": "sha512-EexgmqnyDNyGlISxo7SMrp5MygpJYmqD+0cY5jB6L1U6L4CpKKRWUt8OO1sWzEHDE1+TTvwt+WIFoIWAziOtEA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-dropcursor": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.31.3.tgz",
|
||||
"integrity": "sha512-NWomSfu5CSC7VacnMSDzKT8qm66SzMfZwVPEtwY5bPpRTJgTiT1rNK0neDrrzfMN27MfylGyKWWf7Q5Qf8w/fg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-floating-menu": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.31.3.tgz",
|
||||
"integrity": "sha512-rd4VJ9PGSP9Eop8ZTEwaLZcMzMXLuJKe3hUNf58rq+zANpwM+9fI+vB7g9MAc3eXwUNDxNDVACFIL2oeBqpQ3A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-gapcursor": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.31.3.tgz",
|
||||
"integrity": "sha512-EBXKb1FrVStsNYCcRGtd9jmzveCvR+eqgg1rVqoONrqFK6U7bga6LN+1dMKroP1kliDVgveYkP3vRYxqw+rFqg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-hard-break": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.31.3.tgz",
|
||||
"integrity": "sha512-QAdCvNO4+yW9ATwsrej11NTkDYFqPLIEQr3ARNrKOK1qaiS7A0fia2SEukb/hrkP3A6mbozhoQt2r2RGUf/DpQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-heading": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.31.3.tgz",
|
||||
"integrity": "sha512-rk5VHMAeQcg06SLauN6EGdD2jc0O2qY8QkZYPd0LxNvLblw2BxBx+lxUQSYwLAT9Ie5914gKIK2YbRyO2Ts3ig==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.31.3.tgz",
|
||||
"integrity": "sha512-YnHGy2KShRwvCseAmmxl9VP7R0qaj8QMp3DA6DJWZqp7r5gLGvDkAqhedxqqefqsE4Y43hjkBdjtB9Ce78LkIw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-italic": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.31.3.tgz",
|
||||
"integrity": "sha512-ibGvdvAPyfxBMUVNRI43eb9h2/Jka1MRG5GtnGqbcCX/2+Y/y0EOfFrPQPkuYphiWQYyu9+FzPKMB/pjuaLuKQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-link": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.31.3.tgz",
|
||||
"integrity": "sha512-986wOQzTL9Zr5lf84LCLpm+YOms8A0K39/8DVoqRfebqcOe0/eq4bnztmAlfabOd+kJY92g3AgZERFUx/w+dcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"linkifyjs": "^4.3.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.31.3.tgz",
|
||||
"integrity": "sha512-LoveGnC0FVdCV4jUNBaG1ZA+KWE07+adzV3kGy6uUYFcJEjbVUHTnPDrBOob3IwOSO3sCwIkvQb6EVYeXn/4yg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-item": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.31.3.tgz",
|
||||
"integrity": "sha512-4QlKOriJJMvg95QJTEsy9BUYPBQ6UvJyb8WURRwdUtQUkkqb8h32lg/eyQUv2FzW9IT1AdUyNZfVaxH64kRfQA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-keymap": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.31.3.tgz",
|
||||
"integrity": "sha512-If8UOEdDZbPJU6iYTvLtH6DOp2KBy6BKxg9UELL1AevVetGHEF/7lW8hP50Gn1tHMVpPRqmhSzVrJRpEJJgb/Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-ordered-list": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.31.3.tgz",
|
||||
"integrity": "sha512-mp3g11NgA/PYu8rj7J7Ez3l4qBy6WfTSmHIG4PZvEGG5w2oUAIkgb9DU7nPPzjmeme27oazFYZw+AtZA0+u4tw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-paragraph": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.31.3.tgz",
|
||||
"integrity": "sha512-+iPku7wJfy5hbNDNLX8dveFtYVsZMmh7vztjuq8hT3mipSC4IByDehDbU8fzUCjfXiEzmI7mQn7c8LGmHULuzA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-strike": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.31.3.tgz",
|
||||
"integrity": "sha512-G29bhKttYwcKHT+BI6emWVFol3RO/gUXxQVcmr/iT8LXXy7j8J6HFUnsKM+Kg5YlP1rxMRgsa65dbrQVahZi0A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.31.3.tgz",
|
||||
"integrity": "sha512-gdsWtF+taeaCu6V+5Ct10fGo0ACUy1GnYtbb+mcathBt8OqbT+Ws60p/yEmKesBDz2Hn+B5IcWy6+2BBZl5ZTg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text-style": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.31.3.tgz",
|
||||
"integrity": "sha512-wgjWWrjZwRZHiaDTQPX2am1y/4ePgRgGWF/2MOfSb7g4d5229p4aVtbT1JT7wu9z8OC482pEqcTlhdvgftvW7A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-underline": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.31.3.tgz",
|
||||
"integrity": "sha512-HghdJaOwRqYzsAxqSyNyb+IWyOMcdCl8IoiBETA9BZCJAqdXzFLcuWp7CqCqJPam9dgkWosSoHqTCAO4nTBfpw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extensions": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.31.3.tgz",
|
||||
"integrity": "sha512-8sJNPGGUe8f3aDojcOW5cfVL7I5NrBbE0UWxG08qoi9Tea6qWbvQJsCR9tsrOapr/DaLr3kpbGZ9s1gEEfcNcA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.31.3.tgz",
|
||||
"integrity": "sha512-sZime0SWsz/k62W2WvHx5Ig7G2h7kVhrrmnqy+wEgIHfDwEfOlelRjaWCiBCFlF7dxGUntJusCh9FxlLhni0Ag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.4.1",
|
||||
"prosemirror-commands": "^1.7.1",
|
||||
"prosemirror-dropcursor": "^1.8.2",
|
||||
"prosemirror-gapcursor": "^1.4.1",
|
||||
"prosemirror-history": "^1.5.0",
|
||||
"prosemirror-inputrules": "^1.5.1",
|
||||
"prosemirror-keymap": "^1.2.3",
|
||||
"prosemirror-model": "^1.25.11",
|
||||
"prosemirror-schema-list": "^1.5.1",
|
||||
"prosemirror-state": "^1.4.4",
|
||||
"prosemirror-tables": "^1.8.5",
|
||||
"prosemirror-transform": "^1.12.0",
|
||||
"prosemirror-view": "^1.42.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/react": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.31.3.tgz",
|
||||
"integrity": "sha512-QiwQqvaLFLm5EMFu5tg7nAgXJxCUiUTLD8EsK+TqVV5P4bqOoMOCM39khbhXTJyahCuYpiFWx5YOSDtC/JiPtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"fast-equals": "^5.3.3",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tiptap/extension-bubble-menu": "^3.31.3",
|
||||
"@tiptap/extension-floating-menu": "^3.31.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit": {
|
||||
"version": "3.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.31.3.tgz",
|
||||
"integrity": "sha512-WKof9RewdmGHvWJ1wn0/HVNG2mV+HOgVRyJkKekuM9fgr6BZAAH/xZsWE1eon+94JnQ+KtK2ThydXQM/qc6b2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "3.31.3",
|
||||
"@tiptap/extension-blockquote": "3.31.3",
|
||||
"@tiptap/extension-bold": "3.31.3",
|
||||
"@tiptap/extension-bullet-list": "3.31.3",
|
||||
"@tiptap/extension-code": "3.31.3",
|
||||
"@tiptap/extension-code-block": "3.31.3",
|
||||
"@tiptap/extension-document": "3.31.3",
|
||||
"@tiptap/extension-dropcursor": "3.31.3",
|
||||
"@tiptap/extension-gapcursor": "3.31.3",
|
||||
"@tiptap/extension-hard-break": "3.31.3",
|
||||
"@tiptap/extension-heading": "3.31.3",
|
||||
"@tiptap/extension-horizontal-rule": "3.31.3",
|
||||
"@tiptap/extension-italic": "3.31.3",
|
||||
"@tiptap/extension-link": "3.31.3",
|
||||
"@tiptap/extension-list": "3.31.3",
|
||||
"@tiptap/extension-list-item": "3.31.3",
|
||||
"@tiptap/extension-list-keymap": "3.31.3",
|
||||
"@tiptap/extension-ordered-list": "3.31.3",
|
||||
"@tiptap/extension-paragraph": "3.31.3",
|
||||
"@tiptap/extension-strike": "3.31.3",
|
||||
"@tiptap/extension-text": "3.31.3",
|
||||
"@tiptap/extension-underline": "3.31.3",
|
||||
"@tiptap/extensions": "3.31.3",
|
||||
"@tiptap/pm": "3.31.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
|
||||
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
|
||||
"integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz",
|
||||
@@ -351,6 +852,13 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -361,6 +869,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.2.tgz",
|
||||
"integrity": "sha512-Ywe6jodPTWOTL9/k0bV7gdfP8twKL5Y8I8CZ933fAY5gBekICZSUQTbyH6ut2NZCNyB05mSUwAuEqdEIaOOlDQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -666,6 +1183,12 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/linkifyjs": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
|
||||
"integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.19",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
|
||||
@@ -685,6 +1208,12 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/orderedmap": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -734,6 +1263,145 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-changeset": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.2.tgz",
|
||||
"integrity": "sha512-ViYrjMSg3YFiXwIhKaluu+/mi3Yrxt6AR8ri14ulTaGcZtXO1CThl7A2gv79qx5fQnOw8woKwyBU2u+9PVCm3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-commands": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz",
|
||||
"integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-dropcursor": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz",
|
||||
"integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0",
|
||||
"prosemirror-view": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-gapcursor": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
||||
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-view": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-history": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.31.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-inputrules": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-keymap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"w3c-keyname": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-model": {
|
||||
"version": "1.25.11",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz",
|
||||
"integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-list": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-state": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-tables": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.2.3",
|
||||
"prosemirror-model": "^1.25.4",
|
||||
"prosemirror-state": "^1.4.4",
|
||||
"prosemirror-transform": "^1.10.5",
|
||||
"prosemirror-view": "^1.41.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-transform": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.1.tgz",
|
||||
"integrity": "sha512-t4F5615FycnCqsX7ShTUs8+jfnwcf46kuFRvSl/3qFx2QTZ4DjgowesLHQXcFP7G//TjesqZG3WtgL7vdyVJyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-view": {
|
||||
"version": "1.42.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.3.tgz",
|
||||
"integrity": "sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.25.8",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
|
||||
@@ -827,6 +1495,12 @@
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/rope-sequence": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
|
||||
@@ -866,6 +1540,15 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz",
|
||||
"integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz",
|
||||
@@ -943,6 +1626,12 @@
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/extension-text-style": "^3.31.3",
|
||||
"@tiptap/pm": "^3.31.3",
|
||||
"@tiptap/react": "^3.31.3",
|
||||
"@tiptap/starter-kit": "^3.31.3",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-router-dom": "^7.18.3"
|
||||
|
||||
@@ -7,6 +7,7 @@ import BlogPage from "./pages/BlogPage";
|
||||
import BlogPostPage from "./pages/BlogPostPage";
|
||||
import ContactPage from "./pages/ContactPage";
|
||||
import ExperiencePage from "./pages/ExperiencePage";
|
||||
import JournalAdminPage from "./pages/JournalAdminPage";
|
||||
import NotFoundPage from "./pages/NotFoundPage";
|
||||
import SkillsPage from "./pages/SkillsPage";
|
||||
|
||||
@@ -45,6 +46,7 @@ export default function App() {
|
||||
<Route path="/experience" element={<ExperiencePage />} />
|
||||
<Route path="/skills" element={<SkillsPage />} />
|
||||
<Route path="/blog" element={<BlogPage />} />
|
||||
<Route path="/blog/manage" element={<JournalAdminPage />} />
|
||||
<Route path="/blog/:slug" element={<BlogPostPage />} />
|
||||
<Route
|
||||
path="/contact"
|
||||
@@ -55,4 +57,3 @@ export default function App() {
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+19
-2
@@ -17,7 +17,9 @@ async function request(path, options = {}) {
|
||||
} catch {
|
||||
// Keep the friendly fallback when a proxy or server returns non-JSON.
|
||||
}
|
||||
throw new Error(detail);
|
||||
const error = new Error(detail);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
@@ -28,9 +30,24 @@ export const getExperience = (signal) => request("/api/experience", { signal });
|
||||
export const getSkills = (signal) => request("/api/skills", { signal });
|
||||
export const getPosts = (signal) => request("/api/posts", { signal });
|
||||
export const getPost = (slug, signal) => request(`/api/posts/${slug}`, { signal });
|
||||
export const loginAdmin = (password) =>
|
||||
request("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
export const getAdminSession = (token, signal) =>
|
||||
request("/api/auth/session", {
|
||||
signal,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
export const createPost = (payload, token) =>
|
||||
request("/api/posts", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
export const sendContactMessage = (payload) =>
|
||||
request("/api/contact", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
|
||||
@@ -27,10 +27,38 @@ const paths = {
|
||||
<rect x="3" y="3" width="18" height="18" rx="4" />
|
||||
</>
|
||||
),
|
||||
instagram: (
|
||||
<>
|
||||
<rect x="3" y="3" width="18" height="18" rx="5" />
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M17.5 6.5h.01" />
|
||||
</>
|
||||
),
|
||||
lock: (
|
||||
<>
|
||||
<rect x="4" y="10" width="16" height="11" rx="3" />
|
||||
<path d="M8 10V7a4 4 0 0 1 8 0v3M12 14v3" />
|
||||
</>
|
||||
),
|
||||
logout: <path d="M10 5H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h5m4-4 3-3-3-3m3 3H8" />,
|
||||
menu: <path d="M4 7h16M4 12h16M4 17h16" />,
|
||||
plus: <path d="M12 5v14M5 12h14" />,
|
||||
search: <path d="m20 20-4.35-4.35m2.35-5.15a7.5 7.5 0 1 1-15 0 7.5 7.5 0 0 1 15 0Z" />,
|
||||
send: <path d="m21 3-7.5 18-3.8-7.7L2 9.5 21 3Zm-11.3 10.3L14 9" />,
|
||||
spark: <path d="M12 2c.6 5.4 3.6 8.4 9 9-5.4.6-8.4 3.6-9 9-.6-5.4-3.6-8.4-9-9 5.4-.6 8.4-3.6 9-9Z" />,
|
||||
spotify: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M7.5 9.5c3.5-1 7.3-.6 10 1M8.3 12.5c2.8-.7 6-.4 8.4.8M9 15.3c2.2-.5 4.6-.2 6.6.8" />
|
||||
</>
|
||||
),
|
||||
steam: (
|
||||
<>
|
||||
<circle cx="15.5" cy="8.5" r="3.5" />
|
||||
<circle cx="7" cy="16.5" r="2.5" />
|
||||
<path d="m9 15 3.7-2.4 2.8-.6M4.8 15.3 2.5 14" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
export default function Icon({ name, size = 20, className = "" }) {
|
||||
@@ -51,4 +79,3 @@ export default function Icon({ name, size = 20, className = "" }) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,11 +51,15 @@ export default function BlogPage() {
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
getPosts(controller.signal)
|
||||
.then(setPosts)
|
||||
.then((result) => {
|
||||
if (!controller.signal.aborted) setPosts(result);
|
||||
})
|
||||
.catch((requestError) => {
|
||||
if (requestError.name !== "AbortError") setError(requestError.message);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
@@ -80,7 +84,12 @@ export default function BlogPage() {
|
||||
eyebrow="Journal"
|
||||
title="Notes from the build."
|
||||
description="Practical observations on applied AI, resilient products, and the technology choices behind them."
|
||||
aside={<p className="issue-count">{posts.length || 10}<span>field notes</span></p>}
|
||||
aside={(
|
||||
<div className="journal-heading-aside">
|
||||
<p className="issue-count">{posts.length || 10}<span>field notes</span></p>
|
||||
<Link className="write-link" to="/blog/manage"><Icon name="plus" size={15} /> Write</Link>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="journal-tools reveal">
|
||||
@@ -129,4 +138,3 @@ export default function BlogPage() {
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,17 @@ export default function BlogPostPage() {
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
getPost(slug, controller.signal)
|
||||
.then(setPost)
|
||||
.then((result) => {
|
||||
if (!controller.signal.aborted) setPost(result);
|
||||
})
|
||||
.catch((requestError) => {
|
||||
if (requestError.name !== "AbortError") setError(requestError.message);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [slug]);
|
||||
|
||||
@@ -38,6 +43,10 @@ export default function BlogPostPage() {
|
||||
return <div className="container content-page"><ErrorState message={error} /></div>;
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
return <div className="container content-page"><ErrorState message="This journal note could not be opened." /></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="article-page">
|
||||
<header className={`article-hero article-hero--${post.accent}`}>
|
||||
@@ -72,4 +81,3 @@ export default function BlogPostPage() {
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function ContactPage({ profile, error }) {
|
||||
}
|
||||
|
||||
const socials = profile?.socials ?? [];
|
||||
const interests = profile?.interests ?? [];
|
||||
|
||||
return (
|
||||
<section className="content-page container contact-page">
|
||||
@@ -120,6 +121,27 @@ export default function ContactPage({ profile, error }) {
|
||||
<p>I’m especially interested in full-stack product work, applied AI, and systems that make demanding workflows feel calmer.</p>
|
||||
</div>
|
||||
|
||||
<div className="culture-card">
|
||||
<p className="eyebrow">Beyond the build</p>
|
||||
<h2><strong>I love music</strong> and nearly always have something playing on Spotify.</h2>
|
||||
<p>Off the clock, I share a little life on Instagram and occasionally disappear into a game on Steam.</p>
|
||||
<div className="culture-links">
|
||||
{interests.map((interest) => (
|
||||
<a
|
||||
className={`culture-link culture-link--${interest.kind}`}
|
||||
href={interest.href}
|
||||
key={interest.kind}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="culture-link__icon"><Icon name={interest.kind} /></span>
|
||||
<span><small>{interest.note}</small><strong>{interest.label}</strong></span>
|
||||
<Icon name="arrowUpRight" size={18} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="social-list">
|
||||
<p className="eyebrow">Find me here</p>
|
||||
{socials.map((social) => (
|
||||
@@ -139,4 +161,3 @@ export default function ContactPage({ profile, error }) {
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createPost, getAdminSession, loginAdmin } from "../api";
|
||||
import Icon from "../components/Icon";
|
||||
import PageHeader from "../components/PageHeader";
|
||||
import { LoadingState } from "../components/Status";
|
||||
|
||||
const TOKEN_KEY = "alex-journal-admin";
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
const initialPost = {
|
||||
title: "",
|
||||
excerpt: "",
|
||||
published_at: today,
|
||||
read_time: 5,
|
||||
tags: "AI, Engineering",
|
||||
accent: "mint",
|
||||
section_heading: "The idea",
|
||||
body: "",
|
||||
};
|
||||
|
||||
export default function JournalAdminPage() {
|
||||
const navigate = useNavigate();
|
||||
const [token, setToken] = useState(() => sessionStorage.getItem(TOKEN_KEY) ?? "");
|
||||
const [checking, setChecking] = useState(Boolean(token));
|
||||
const [password, setPassword] = useState("");
|
||||
const [post, setPost] = useState(initialPost);
|
||||
const [status, setStatus] = useState({ type: "idle", message: "" });
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setChecking(false);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
getAdminSession(token, controller.signal)
|
||||
.catch((error) => {
|
||||
if (error.name !== "AbortError") {
|
||||
sessionStorage.removeItem(TOKEN_KEY);
|
||||
setToken("");
|
||||
setStatus({ type: "error", message: "Your session ended. Sign in again." });
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) setChecking(false);
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [token]);
|
||||
|
||||
async function signIn(event) {
|
||||
event.preventDefault();
|
||||
setStatus({ type: "sending", message: "Signing in…" });
|
||||
try {
|
||||
const result = await loginAdmin(password);
|
||||
sessionStorage.setItem(TOKEN_KEY, result.access_token);
|
||||
setToken(result.access_token);
|
||||
setPassword("");
|
||||
setStatus({ type: "success", message: "Signed in. Your session lasts four hours." });
|
||||
} catch (error) {
|
||||
setStatus({ type: "error", message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
function signOut() {
|
||||
sessionStorage.removeItem(TOKEN_KEY);
|
||||
setToken("");
|
||||
setStatus({ type: "idle", message: "" });
|
||||
}
|
||||
|
||||
function updatePost(event) {
|
||||
setPost((current) => ({ ...current, [event.target.name]: event.target.value }));
|
||||
}
|
||||
|
||||
async function publish(event) {
|
||||
event.preventDefault();
|
||||
const paragraphs = post.body
|
||||
.split(/\n\s*\n/)
|
||||
.map((paragraph) => paragraph.trim())
|
||||
.filter(Boolean);
|
||||
const payload = {
|
||||
title: post.title,
|
||||
excerpt: post.excerpt,
|
||||
published_at: post.published_at,
|
||||
read_time: Number(post.read_time),
|
||||
tags: post.tags.split(",").map((tag) => tag.trim()).filter(Boolean),
|
||||
accent: post.accent,
|
||||
content: [{ heading: post.section_heading, paragraphs }],
|
||||
};
|
||||
|
||||
setStatus({ type: "sending", message: "Publishing…" });
|
||||
try {
|
||||
const created = await createPost(payload, token);
|
||||
setStatus({ type: "success", message: "Published. Opening the article…" });
|
||||
navigate(`/blog/${created.slug}`);
|
||||
} catch (error) {
|
||||
if (error.status === 401) {
|
||||
sessionStorage.removeItem(TOKEN_KEY);
|
||||
setToken("");
|
||||
}
|
||||
setStatus({ type: "error", message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="content-page container publisher-page">
|
||||
<PageHeader
|
||||
eyebrow="Journal studio"
|
||||
title="Publish a field note."
|
||||
description="A small private writing room for adding new articles to the JSON journal."
|
||||
aside={token ? <span className="publisher-badge"><span /> Authenticated</span> : null}
|
||||
/>
|
||||
|
||||
{checking && <LoadingState label="Checking your session" />}
|
||||
|
||||
{!checking && !token && (
|
||||
<form className="publisher-login reveal" onSubmit={signIn}>
|
||||
<span className="publisher-login__icon"><Icon name="lock" size={24} /></span>
|
||||
<div>
|
||||
<p className="eyebrow">Private access</p>
|
||||
<h2>Sign in to write</h2>
|
||||
<p>The publisher uses one server-side password. It is never stored in the browser.</p>
|
||||
</div>
|
||||
<label>
|
||||
<span>Admin password</span>
|
||||
<input
|
||||
autoComplete="current-password"
|
||||
autoFocus
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="Enter your journal password"
|
||||
required
|
||||
type="password"
|
||||
value={password}
|
||||
/>
|
||||
</label>
|
||||
<button className="button button--primary" disabled={status.type === "sending"} type="submit">
|
||||
Unlock publisher <Icon name="arrow" size={18} />
|
||||
</button>
|
||||
{status.message && <p className={`form-status form-status--${status.type}`} role="status">{status.message}</p>}
|
||||
</form>
|
||||
)}
|
||||
|
||||
{!checking && token && (
|
||||
<form className="publisher-form reveal" onSubmit={publish}>
|
||||
<div className="publisher-toolbar">
|
||||
<div>
|
||||
<p className="eyebrow">New article</p>
|
||||
<p>Paragraphs are separated by a blank line.</p>
|
||||
</div>
|
||||
<button className="text-button" onClick={signOut} type="button"><Icon name="logout" size={16} /> Sign out</button>
|
||||
</div>
|
||||
|
||||
<label className="publisher-field publisher-field--wide">
|
||||
<span>Title</span>
|
||||
<input minLength="5" name="title" onChange={updatePost} placeholder="A clear, useful title" required value={post.title} />
|
||||
</label>
|
||||
|
||||
<label className="publisher-field publisher-field--wide">
|
||||
<span>Excerpt</span>
|
||||
<textarea maxLength="360" minLength="20" name="excerpt" onChange={updatePost} placeholder="A concise introduction for the journal index." required rows="3" value={post.excerpt} />
|
||||
</label>
|
||||
|
||||
<div className="publisher-fields-row">
|
||||
<label className="publisher-field">
|
||||
<span>Publish date</span>
|
||||
<input name="published_at" onChange={updatePost} required type="date" value={post.published_at} />
|
||||
</label>
|
||||
<label className="publisher-field">
|
||||
<span>Reading time</span>
|
||||
<input max="60" min="1" name="read_time" onChange={updatePost} required type="number" value={post.read_time} />
|
||||
</label>
|
||||
<label className="publisher-field">
|
||||
<span>Color</span>
|
||||
<select name="accent" onChange={updatePost} value={post.accent}>
|
||||
<option value="mint">Mint</option>
|
||||
<option value="blue">Blue</option>
|
||||
<option value="lavender">Lavender</option>
|
||||
<option value="peach">Peach</option>
|
||||
<option value="yellow">Yellow</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="publisher-field publisher-field--wide">
|
||||
<span>Topics</span>
|
||||
<input name="tags" onChange={updatePost} placeholder="AI, React, Systems" required value={post.tags} />
|
||||
<small>Separate up to six topics with commas.</small>
|
||||
</label>
|
||||
|
||||
<label className="publisher-field publisher-field--wide">
|
||||
<span>Section heading</span>
|
||||
<input minLength="3" name="section_heading" onChange={updatePost} required value={post.section_heading} />
|
||||
</label>
|
||||
|
||||
<label className="publisher-field publisher-field--wide">
|
||||
<span>Article</span>
|
||||
<textarea minLength="40" name="body" onChange={updatePost} placeholder="Write the article here…" required rows="15" value={post.body} />
|
||||
</label>
|
||||
|
||||
<div className="publisher-submit">
|
||||
<button className="button button--primary" disabled={status.type === "sending"} type="submit">
|
||||
<Icon name="plus" size={18} /> {status.type === "sending" ? "Publishing…" : "Publish article"}
|
||||
</button>
|
||||
{status.message && <p className={`form-status form-status--${status.type}`} role="status">{status.message}</p>}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+323
-14
@@ -25,7 +25,7 @@
|
||||
--container: 1160px;
|
||||
--gutter: clamp(24px, 8vw, 150px);
|
||||
color: var(--ink);
|
||||
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-family: "Roboto Flex", Roboto, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
@@ -171,7 +171,7 @@ button {
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 5px;
|
||||
border: 1px solid rgba(54, 78, 70, 0.08);
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.67);
|
||||
box-shadow: 0 8px 28px rgba(46, 65, 58, 0.05);
|
||||
@@ -650,7 +650,7 @@ button {
|
||||
|
||||
.timeline-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
box-shadow: 0 10px 38px rgba(46, 65, 58, 0.04);
|
||||
@@ -728,7 +728,7 @@ button {
|
||||
height: 38px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border: 1px solid var(--line);
|
||||
border: 0;
|
||||
border-radius: 13px;
|
||||
background: rgba(247, 248, 244, 0.78);
|
||||
transition: transform 200ms ease, background 200ms ease;
|
||||
@@ -809,7 +809,7 @@ button {
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 12px 13px;
|
||||
border: 1px solid var(--line);
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: #fafbf8;
|
||||
transition: background 160ms ease, transform 160ms ease;
|
||||
@@ -874,7 +874,7 @@ button {
|
||||
gap: 30px;
|
||||
margin-bottom: 22px;
|
||||
padding: 25px 28px;
|
||||
border: 1px solid var(--line);
|
||||
border: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
}
|
||||
@@ -919,7 +919,7 @@ button {
|
||||
grid-column: span 2;
|
||||
min-height: 285px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(54, 78, 70, 0.07);
|
||||
border: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
animation-delay: var(--delay);
|
||||
transition: transform 200ms ease, box-shadow 200ms ease;
|
||||
@@ -999,7 +999,7 @@ button {
|
||||
|
||||
.issue-count span {
|
||||
color: var(--ink-faint);
|
||||
font-family: Inter, ui-sans-serif, sans-serif;
|
||||
font-family: "Roboto Flex", Roboto, ui-sans-serif, sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
@@ -1073,7 +1073,7 @@ button {
|
||||
|
||||
.filter-row button:hover,
|
||||
.filter-row button.is-active {
|
||||
border-color: var(--line);
|
||||
border-color: transparent;
|
||||
color: var(--ink);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
@@ -1086,7 +1086,7 @@ button {
|
||||
|
||||
.post-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(54, 78, 70, 0.08);
|
||||
border: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
box-shadow: 0 8px 28px rgba(46, 65, 58, 0.04);
|
||||
@@ -1231,8 +1231,9 @@ button {
|
||||
height: 34px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border: 1px solid var(--line);
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: rgba(225, 238, 233, 0.65);
|
||||
transition: color 160ms ease, background 160ms ease;
|
||||
}
|
||||
|
||||
@@ -1407,7 +1408,7 @@ button {
|
||||
justify-items: center;
|
||||
margin-top: 70px;
|
||||
padding: 36px;
|
||||
border: 1px solid var(--line);
|
||||
border: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface);
|
||||
text-align: center;
|
||||
@@ -1458,7 +1459,7 @@ button {
|
||||
display: grid;
|
||||
gap: 19px;
|
||||
padding: clamp(25px, 4vw, 40px);
|
||||
border: 1px solid var(--line);
|
||||
border: 0;
|
||||
border-radius: var(--radius-xl);
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
box-shadow: var(--shadow-soft);
|
||||
@@ -1551,7 +1552,7 @@ button {
|
||||
.contact-note,
|
||||
.social-list {
|
||||
padding: 25px;
|
||||
border: 1px solid var(--line);
|
||||
border: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
@@ -1692,6 +1693,283 @@ button {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.journal-heading-aside {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.write-link,
|
||||
.text-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
border: 0;
|
||||
color: var(--sage-deep);
|
||||
background: var(--mint);
|
||||
font-size: 11px;
|
||||
font-weight: 780;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.write-link {
|
||||
min-height: 38px;
|
||||
padding-inline: 14px;
|
||||
border-radius: 13px;
|
||||
box-shadow: 0 8px 22px rgba(47, 81, 71, 0.08);
|
||||
}
|
||||
|
||||
.culture-card {
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(145deg, var(--lavender), #f4f0f5);
|
||||
box-shadow: 0 12px 34px rgba(75, 67, 95, 0.06);
|
||||
}
|
||||
|
||||
.culture-card .eyebrow {
|
||||
margin-bottom: 13px;
|
||||
color: var(--lavender-deep);
|
||||
}
|
||||
|
||||
.culture-card h2 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-family: "Roboto Flex", Roboto, ui-sans-serif, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 520;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.culture-card h2 strong {
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.culture-card > p:not(.eyebrow) {
|
||||
margin: 11px 0 20px;
|
||||
color: var(--ink-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.culture-links {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.culture-link {
|
||||
display: grid;
|
||||
min-height: 58px;
|
||||
grid-template-columns: 38px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 16px;
|
||||
color: var(--ink);
|
||||
box-shadow: 0 9px 24px rgba(46, 50, 58, 0.07);
|
||||
transition: transform 180ms ease, box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.culture-link:hover {
|
||||
box-shadow: 0 13px 29px rgba(46, 50, 58, 0.11);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.culture-link--spotify { background: #dff3e5; }
|
||||
.culture-link--instagram { background: #f6e3ea; }
|
||||
.culture-link--steam { background: #e0ebf3; }
|
||||
|
||||
.culture-link__icon {
|
||||
display: grid;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
border-radius: 13px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.culture-link small,
|
||||
.culture-link strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.culture-link small {
|
||||
margin-bottom: 2px;
|
||||
color: var(--ink-faint);
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.culture-link strong {
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.publisher-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 10px 13px;
|
||||
border-radius: 999px;
|
||||
color: #3b725e;
|
||||
background: var(--mint);
|
||||
font-size: 10px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.publisher-badge span {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #5c947c;
|
||||
}
|
||||
|
||||
.publisher-login {
|
||||
display: grid;
|
||||
max-width: 620px;
|
||||
gap: 22px;
|
||||
margin: -22px auto 30px;
|
||||
padding: clamp(28px, 5vw, 48px);
|
||||
border-radius: var(--radius-xl);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.publisher-login__icon {
|
||||
display: grid;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
place-items: center;
|
||||
border-radius: 17px;
|
||||
color: var(--sage-deep);
|
||||
background: var(--mint);
|
||||
}
|
||||
|
||||
.publisher-login .eyebrow,
|
||||
.publisher-toolbar .eyebrow {
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.publisher-login h2 {
|
||||
margin: 0;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: 34px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.publisher-login > div > p:last-child,
|
||||
.publisher-toolbar > div > p:last-child {
|
||||
margin: 8px 0 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.publisher-login label,
|
||||
.publisher-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.publisher-login label > span,
|
||||
.publisher-field > span {
|
||||
color: var(--ink-soft);
|
||||
font-size: 10px;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.publisher-login input,
|
||||
.publisher-field input,
|
||||
.publisher-field textarea,
|
||||
.publisher-field select {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(54, 78, 70, 0.13);
|
||||
border-radius: 15px;
|
||||
outline: 0;
|
||||
color: var(--ink);
|
||||
background: #fbfcfa;
|
||||
font-size: 13px;
|
||||
transition: border 160ms ease, box-shadow 160ms ease, background 160ms ease;
|
||||
}
|
||||
|
||||
.publisher-login input,
|
||||
.publisher-field input,
|
||||
.publisher-field select {
|
||||
min-height: 50px;
|
||||
padding-inline: 15px;
|
||||
}
|
||||
|
||||
.publisher-field textarea {
|
||||
padding: 15px;
|
||||
line-height: 1.7;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.publisher-login input:focus,
|
||||
.publisher-field input:focus,
|
||||
.publisher-field textarea:focus,
|
||||
.publisher-field select:focus {
|
||||
border-color: rgba(47, 81, 71, 0.43);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px rgba(47, 81, 71, 0.07);
|
||||
}
|
||||
|
||||
.publisher-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 21px;
|
||||
margin-top: -25px;
|
||||
padding: clamp(28px, 5vw, 48px);
|
||||
border-radius: var(--radius-xl);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.publisher-toolbar,
|
||||
.publisher-field--wide,
|
||||
.publisher-submit {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.publisher-toolbar,
|
||||
.publisher-submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.text-button {
|
||||
padding: 10px 13px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.publisher-fields-row {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.publisher-field small {
|
||||
color: var(--ink-faint);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.publisher-submit {
|
||||
justify-content: flex-start;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
:root {
|
||||
--gutter: clamp(24px, 5vw, 70px);
|
||||
@@ -1782,6 +2060,10 @@ button {
|
||||
.contact-aside {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.contact-aside .social-list {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -1940,6 +2222,14 @@ button {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.publisher-fields-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.publisher-toolbar {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
@@ -1957,6 +2247,25 @@ button {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.journal-heading-aside {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.publisher-login,
|
||||
.publisher-form {
|
||||
margin-top: -10px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.publisher-submit {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.publisher-submit .button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.about-page {
|
||||
gap: 40px;
|
||||
padding-block: 50px 70px;
|
||||
|
||||
Reference in New Issue
Block a user