Compare commits
60 Commits
Author | SHA1 | Date | |
---|---|---|---|
b41404e86b | |||
f806365baa | |||
4ce242e87e | |||
60a5577a6b | |||
01da52e855 | |||
3eb42a820e | |||
489060ecd5 | |||
85ce43cc4c | |||
44b1a581cb | |||
0417af9e96 | |||
32590c0d13 | |||
356b92233c | |||
383d1b7c87 | |||
a94da9a450 | |||
58c20d64f1 | |||
c7e179e270 | |||
7a9cc80977 | |||
ac2724951f | |||
0eb1df0bf9 | |||
29a314d1a2 | |||
224ff2a6b4 | |||
12cd9f4fe6 | |||
d142db7fe4 | |||
47bd1899ee | |||
8dbcb6494b | |||
261fa687ea | |||
f081435b17 | |||
bb780b414a | |||
5f7ff6be0a | |||
f6fbd90a17 | |||
2f0ae31852 | |||
75d9c82390 | |||
9a8d072064 | |||
7e2aa13b26 | |||
cee941525f | |||
|
102456d7eb | ||
|
ec9546c49c | ||
|
251e37f104 | ||
|
122753474a | ||
|
3afea9b415 | ||
|
58b5c15fe9 | ||
|
d374d19a8a | ||
|
c6174fab8d | ||
|
768a16c170 | ||
|
ea98b243c4 | ||
|
c3c3fd4baf | ||
|
761d34f5ff | ||
|
9e536660ce | ||
|
67dae9bd8f | ||
|
b636376a45 | ||
|
62b30a0335 | ||
|
17ccb97185 | ||
|
ad4e6e39c9 | ||
|
1222296901 | ||
|
bf603e1e76 | ||
|
bae21571e9 | ||
|
9e2e6579eb | ||
|
d035adcfe9 | ||
|
33d7608eef | ||
|
0eee4e6a07 |
10
.editorconfig
Normal file
|
@ -0,0 +1,10 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,json,yml}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
4
.gitattributes
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
/.pnp.* binary linguist-generated
|
50
.gitignore
vendored
|
@ -1,44 +1,22 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
node_modules/
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# sanity
|
||||
/dist
|
||||
.sanity/
|
||||
|
||||
# env
|
||||
# environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
.vercel
|
||||
|
|
4
.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
/src/app/internal
|
4
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
11
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
BIN
.yarn/install-state.gz
vendored
Normal file
1
.yarnrc.yml
Normal file
|
@ -0,0 +1 @@
|
|||
nodeLinker: node-modules
|
35
README.md
|
@ -1,34 +1,3 @@
|
|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
# jackmerrill.com
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
This is my website! Written using Astro (and some React), styled with TailwindCSS, designed with Figma, and deployed with Vercel.
|
||||
|
|
28
astro.config.mjs
Normal file
|
@ -0,0 +1,28 @@
|
|||
import {
|
||||
defineConfig,
|
||||
passthroughImageService,
|
||||
squooshImageService,
|
||||
} from "astro/config";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import react from "@astrojs/react";
|
||||
|
||||
import vercel from "@astrojs/vercel/serverless";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [tailwind(), react()],
|
||||
output: "hybrid",
|
||||
adapter: vercel({
|
||||
imageService: true,
|
||||
webAnalytics: {
|
||||
enabled: true,
|
||||
},
|
||||
speedInsights: {
|
||||
enabled: true,
|
||||
},
|
||||
functionPerRoute: true,
|
||||
}),
|
||||
// image: {
|
||||
// service: passthroughImageService(),
|
||||
// },
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
domains: ["cdn.sanity.io"],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
19845
package-lock.json
generated
78
package.json
|
@ -1,58 +1,36 @@
|
|||
{
|
||||
"name": "jackmerrill.com",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"name": "jackmerrill-com",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@next/bundle-analyzer": "^13.4.5",
|
||||
"@radix-ui/react-context-menu": "^2.1.3",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-navigation-menu": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@sanity/image-url": "^1.0.2",
|
||||
"@sanity/vision": "^3.11.2",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/node": "20.2.1",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"autoprefixer": "10.4.14",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"easymde": "2",
|
||||
"eslint": "8.41.0",
|
||||
"eslint-config-next": "13.4.3",
|
||||
"groqd": "^0.15.6",
|
||||
"lucide-react": "^0.220.0",
|
||||
"next": "13.4.3",
|
||||
"next-sanity": "^4.3.3",
|
||||
"next-sanity-image": "^6.0.0",
|
||||
"postcss": "8.4.23",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"rehype-raw": "^6.1.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sanity": "^3.11.2",
|
||||
"sanity-plugin-markdown": "^4.1.0",
|
||||
"styled-components": "^5.3.10",
|
||||
"tailwind-merge": "^1.12.0",
|
||||
"tailwindcss": "3.3.2",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"tailwindcss-hero-patterns": "^0.1.2",
|
||||
"twemoji": "^14.0.2",
|
||||
"typescript": "5.0.4"
|
||||
"@astrojs/react": "3.6.2",
|
||||
"@astrojs/tailwind": "5.1.0",
|
||||
"@astrojs/vercel": "7.7.2",
|
||||
"@icons-pack/react-simple-icons": "^9.1.0",
|
||||
"@react-three/drei": "^9.101.0",
|
||||
"@react-three/fiber": "^8.15.19",
|
||||
"@react-three/xr": "^5.7.1",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"astro": "4.14.2",
|
||||
"astro-seo": "^0.8.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"sharp": "^0.33.5",
|
||||
"squoosh": "^0.0.0",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"three": "^0.162.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-syntax-highlighter": "^15.5.7",
|
||||
"@types/twemoji": "^13.1.2"
|
||||
"sass": "^1.69.5",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
182
public/css/sentient.css
Normal file
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* @license
|
||||
*
|
||||
* Font Family: Sentient
|
||||
* Designed by: Noopur Choksi
|
||||
* URL: https://www.fontshare.com/fonts/sentient
|
||||
* © 2023 Indian Type Foundry
|
||||
*
|
||||
* Font Styles:
|
||||
* Sentient Variable(Variable font)
|
||||
* Sentient Variable Italic(Variable font)
|
||||
* Sentient Extralight
|
||||
* Sentient Extralight Italic
|
||||
* Sentient Light
|
||||
* Sentient Light Italic
|
||||
* Sentient Regular
|
||||
* Sentient Italic
|
||||
* Sentient Medium
|
||||
* Sentient Medium Italic
|
||||
* Sentient Bold
|
||||
* Sentient Bold Italic
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* This is a variable font
|
||||
* You can controll variable axes as shown below:
|
||||
* font-variation-settings: 'wght' 700.0 'wght' 400.0;
|
||||
*
|
||||
* available axes:
|
||||
|
||||
* 'wght' (range from 200.0 to 700.0)
|
||||
|
||||
* 'wght' (range from 200.0 to 700.0)
|
||||
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-Variable';
|
||||
src: url('../fonts/Sentient-Variable.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-Variable.woff') format('woff'),
|
||||
url('../fonts/Sentient-Variable.ttf') format('truetype');
|
||||
font-weight: 200 700;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a variable font
|
||||
* You can controll variable axes as shown below:
|
||||
* font-variation-settings: 'wght' 700.0 'wght' 400.0;
|
||||
*
|
||||
* available axes:
|
||||
|
||||
* 'wght' (range from 200.0 to 700.0)
|
||||
|
||||
* 'wght' (range from 200.0 to 700.0)
|
||||
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-VariableItalic';
|
||||
src: url('../fonts/Sentient-VariableItalic.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-VariableItalic.woff') format('woff'),
|
||||
url('../fonts/Sentient-VariableItalic.ttf') format('truetype');
|
||||
font-weight: 200 700;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-Extralight';
|
||||
src: url('../fonts/Sentient-Extralight.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-Extralight.woff') format('woff'),
|
||||
url('../fonts/Sentient-Extralight.ttf') format('truetype');
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-ExtralightItalic';
|
||||
src: url('../fonts/Sentient-ExtralightItalic.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-ExtralightItalic.woff') format('woff'),
|
||||
url('../fonts/Sentient-ExtralightItalic.ttf') format('truetype');
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-Light';
|
||||
src: url('../fonts/Sentient-Light.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-Light.woff') format('woff'),
|
||||
url('../fonts/Sentient-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-LightItalic';
|
||||
src: url('../fonts/Sentient-LightItalic.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-LightItalic.woff') format('woff'),
|
||||
url('../fonts/Sentient-LightItalic.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-Regular';
|
||||
src: url('../fonts/Sentient-Regular.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-Regular.woff') format('woff'),
|
||||
url('../fonts/Sentient-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-Italic';
|
||||
src: url('../fonts/Sentient-Italic.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-Italic.woff') format('woff'),
|
||||
url('../fonts/Sentient-Italic.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-Medium';
|
||||
src: url('../fonts/Sentient-Medium.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-Medium.woff') format('woff'),
|
||||
url('../fonts/Sentient-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-MediumItalic';
|
||||
src: url('../fonts/Sentient-MediumItalic.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-MediumItalic.woff') format('woff'),
|
||||
url('../fonts/Sentient-MediumItalic.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-Bold';
|
||||
src: url('../fonts/Sentient-Bold.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-Bold.woff') format('woff'),
|
||||
url('../fonts/Sentient-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: 'Sentient-BoldItalic';
|
||||
src: url('../fonts/Sentient-BoldItalic.woff2') format('woff2'),
|
||||
url('../fonts/Sentient-BoldItalic.woff') format('woff'),
|
||||
url('../fonts/Sentient-BoldItalic.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
}
|
||||
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/fonts/Sentient-Bold.eot
Normal file
BIN
public/fonts/Sentient-Bold.ttf
Normal file
BIN
public/fonts/Sentient-Bold.woff
Normal file
BIN
public/fonts/Sentient-Bold.woff2
Normal file
BIN
public/fonts/Sentient-BoldItalic.eot
Normal file
BIN
public/fonts/Sentient-BoldItalic.ttf
Normal file
BIN
public/fonts/Sentient-BoldItalic.woff
Normal file
BIN
public/fonts/Sentient-BoldItalic.woff2
Normal file
BIN
public/fonts/Sentient-Extralight.eot
Normal file
BIN
public/fonts/Sentient-Extralight.ttf
Normal file
BIN
public/fonts/Sentient-Extralight.woff
Normal file
BIN
public/fonts/Sentient-Extralight.woff2
Normal file
BIN
public/fonts/Sentient-ExtralightItalic.eot
Normal file
BIN
public/fonts/Sentient-ExtralightItalic.ttf
Normal file
BIN
public/fonts/Sentient-ExtralightItalic.woff
Normal file
BIN
public/fonts/Sentient-ExtralightItalic.woff2
Normal file
BIN
public/fonts/Sentient-Italic.eot
Normal file
BIN
public/fonts/Sentient-Italic.ttf
Normal file
BIN
public/fonts/Sentient-Italic.woff
Normal file
BIN
public/fonts/Sentient-Italic.woff2
Normal file
BIN
public/fonts/Sentient-Light.eot
Normal file
BIN
public/fonts/Sentient-Light.ttf
Normal file
BIN
public/fonts/Sentient-Light.woff
Normal file
BIN
public/fonts/Sentient-Light.woff2
Normal file
BIN
public/fonts/Sentient-LightItalic.eot
Normal file
BIN
public/fonts/Sentient-LightItalic.ttf
Normal file
BIN
public/fonts/Sentient-LightItalic.woff
Normal file
BIN
public/fonts/Sentient-LightItalic.woff2
Normal file
BIN
public/fonts/Sentient-Medium.eot
Normal file
BIN
public/fonts/Sentient-Medium.ttf
Normal file
BIN
public/fonts/Sentient-Medium.woff
Normal file
BIN
public/fonts/Sentient-Medium.woff2
Normal file
BIN
public/fonts/Sentient-MediumItalic.eot
Normal file
BIN
public/fonts/Sentient-MediumItalic.ttf
Normal file
BIN
public/fonts/Sentient-MediumItalic.woff
Normal file
BIN
public/fonts/Sentient-MediumItalic.woff2
Normal file
BIN
public/fonts/Sentient-Regular.eot
Normal file
BIN
public/fonts/Sentient-Regular.ttf
Normal file
BIN
public/fonts/Sentient-Regular.woff
Normal file
BIN
public/fonts/Sentient-Regular.woff2
Normal file
BIN
public/fonts/Sentient-Variable.eot
Normal file
BIN
public/fonts/Sentient-Variable.ttf
Normal file
BIN
public/fonts/Sentient-Variable.woff
Normal file
BIN
public/fonts/Sentient-Variable.woff2
Normal file
BIN
public/fonts/Sentient-VariableItalic.eot
Normal file
BIN
public/fonts/Sentient-VariableItalic.ttf
Normal file
BIN
public/fonts/Sentient-VariableItalic.woff
Normal file
BIN
public/fonts/Sentient-VariableItalic.woff2
Normal file
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
BIN
public/resume.pdf
Normal file
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
Before Width: | Height: | Size: 629 B |
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* This configuration file lets you run `$ sanity [command]` in this folder
|
||||
* Go to https://www.sanity.io/docs/cli to learn more.
|
||||
**/
|
||||
import { defineCliConfig } from 'sanity/cli'
|
||||
|
||||
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
|
||||
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
|
||||
|
||||
export default defineCliConfig({ api: { projectId, dataset } })
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* This configuration is used to for the Sanity Studio that’s mounted on the `/app/internal/studio/[[...index]]/page.tsx` route
|
||||
*/
|
||||
|
||||
import { visionTool } from "@sanity/vision";
|
||||
import { defineConfig } from "sanity";
|
||||
import { deskTool } from "sanity/desk";
|
||||
import { markdownSchema } from "sanity-plugin-markdown";
|
||||
|
||||
// Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works
|
||||
import { apiVersion, dataset, projectId } from "./sanity/env";
|
||||
import { schema } from "./sanity/schema";
|
||||
|
||||
export default defineConfig({
|
||||
basePath: "/internal/studio",
|
||||
projectId,
|
||||
dataset,
|
||||
// Add and edit the content schema in the './sanity/schema' folder
|
||||
schema,
|
||||
plugins: [
|
||||
deskTool(),
|
||||
// Vision is a tool that lets you query your content with GROQ in the studio
|
||||
// https://www.sanity.io/docs/the-vision-plugin
|
||||
visionTool({ defaultApiVersion: apiVersion }),
|
||||
markdownSchema(),
|
||||
],
|
||||
});
|
|
@ -1,22 +0,0 @@
|
|||
export const apiVersion =
|
||||
process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2023-05-25";
|
||||
|
||||
export const dataset = assertValue(
|
||||
process.env.NEXT_PUBLIC_SANITY_DATASET,
|
||||
"Missing environment variable: NEXT_PUBLIC_SANITY_DATASET"
|
||||
);
|
||||
|
||||
export const projectId = assertValue(
|
||||
process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
|
||||
"Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID"
|
||||
);
|
||||
|
||||
export const useCdn = false;
|
||||
|
||||
function assertValue<T>(v: T | undefined, errorMessage: string): T {
|
||||
if (v === undefined) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { createClient } from 'next-sanity'
|
||||
|
||||
import { apiVersion, dataset, projectId, useCdn } from '../env'
|
||||
|
||||
export const client = createClient({
|
||||
apiVersion,
|
||||
dataset,
|
||||
projectId,
|
||||
useCdn,
|
||||
})
|
|
@ -1,13 +0,0 @@
|
|||
import createImageUrlBuilder from '@sanity/image-url'
|
||||
import type { Image } from 'sanity'
|
||||
|
||||
import { dataset, projectId } from '../env'
|
||||
|
||||
const imageBuilder = createImageUrlBuilder({
|
||||
projectId: projectId || '',
|
||||
dataset: dataset || '',
|
||||
})
|
||||
|
||||
export const urlForImage = (source: Image) => {
|
||||
return imageBuilder?.image(source).auto('format').fit('max')
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { type SchemaTypeDefinition } from "sanity";
|
||||
|
||||
import blockContent from "./schemas/blockContent";
|
||||
import category from "./schemas/category";
|
||||
import post from "./schemas/post";
|
||||
import author from "./schemas/author";
|
||||
import project from "./schemas/project";
|
||||
|
||||
export const schema: { types: SchemaTypeDefinition[] } = {
|
||||
types: [post, author, category, blockContent, project],
|
||||
};
|
|
@ -1,57 +0,0 @@
|
|||
import { defineField, defineType } from "sanity";
|
||||
|
||||
export default defineType({
|
||||
name: "author",
|
||||
title: "Author",
|
||||
type: "document",
|
||||
fields: [
|
||||
defineField({
|
||||
name: "name",
|
||||
title: "Name",
|
||||
type: "string",
|
||||
}),
|
||||
defineField({
|
||||
name: "slug",
|
||||
title: "Slug",
|
||||
type: "slug",
|
||||
options: {
|
||||
source: "name",
|
||||
maxLength: 96,
|
||||
},
|
||||
}),
|
||||
defineField({
|
||||
name: "image",
|
||||
title: "Image",
|
||||
type: "image",
|
||||
options: {
|
||||
hotspot: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "alt",
|
||||
type: "string",
|
||||
title: "Alternative Text",
|
||||
},
|
||||
],
|
||||
}),
|
||||
defineField({
|
||||
name: "bio",
|
||||
title: "Bio",
|
||||
type: "array",
|
||||
of: [
|
||||
{
|
||||
title: "Block",
|
||||
type: "block",
|
||||
styles: [{ title: "Normal", value: "normal" }],
|
||||
lists: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: "name",
|
||||
media: "image",
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,75 +0,0 @@
|
|||
import {defineType, defineArrayMember} from 'sanity'
|
||||
|
||||
/**
|
||||
* This is the schema type for block content used in the post document type
|
||||
* Importing this type into the studio configuration's `schema` property
|
||||
* lets you reuse it in other document types with:
|
||||
* {
|
||||
* name: 'someName',
|
||||
* title: 'Some title',
|
||||
* type: 'blockContent'
|
||||
* }
|
||||
*/
|
||||
|
||||
export default defineType({
|
||||
title: 'Block Content',
|
||||
name: 'blockContent',
|
||||
type: 'array',
|
||||
of: [
|
||||
defineArrayMember({
|
||||
title: 'Block',
|
||||
type: 'block',
|
||||
// Styles let you define what blocks can be marked up as. The default
|
||||
// set corresponds with HTML tags, but you can set any title or value
|
||||
// you want, and decide how you want to deal with it where you want to
|
||||
// use your content.
|
||||
styles: [
|
||||
{title: 'Normal', value: 'normal'},
|
||||
{title: 'H1', value: 'h1'},
|
||||
{title: 'H2', value: 'h2'},
|
||||
{title: 'H3', value: 'h3'},
|
||||
{title: 'H4', value: 'h4'},
|
||||
{title: 'Quote', value: 'blockquote'},
|
||||
],
|
||||
lists: [{title: 'Bullet', value: 'bullet'}],
|
||||
// Marks let you mark up inline text in the Portable Text Editor
|
||||
marks: {
|
||||
// Decorators usually describe a single property – e.g. a typographic
|
||||
// preference or highlighting
|
||||
decorators: [
|
||||
{title: 'Strong', value: 'strong'},
|
||||
{title: 'Emphasis', value: 'em'},
|
||||
],
|
||||
// Annotations can be any object structure – e.g. a link or a footnote.
|
||||
annotations: [
|
||||
{
|
||||
title: 'URL',
|
||||
name: 'link',
|
||||
type: 'object',
|
||||
fields: [
|
||||
{
|
||||
title: 'URL',
|
||||
name: 'href',
|
||||
type: 'url',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
// You can add additional types here. Note that you can't use
|
||||
// primitive types such as 'string' and 'number' in the same array
|
||||
// as a block type.
|
||||
defineArrayMember({
|
||||
type: 'image',
|
||||
options: {hotspot: true},
|
||||
fields: [
|
||||
{
|
||||
name: 'alt',
|
||||
type: 'string',
|
||||
title: 'Alternative Text',
|
||||
}
|
||||
]
|
||||
}),
|
||||
],
|
||||
})
|
|
@ -1,19 +0,0 @@
|
|||
import {defineField, defineType} from 'sanity'
|
||||
|
||||
export default defineType({
|
||||
name: 'category',
|
||||
title: 'Category',
|
||||
type: 'document',
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
}),
|
||||
defineField({
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
type: 'text',
|
||||
}),
|
||||
],
|
||||
})
|
|
@ -1,80 +0,0 @@
|
|||
import { defineField, defineType } from "sanity";
|
||||
|
||||
export default defineType({
|
||||
name: "post",
|
||||
title: "Post",
|
||||
type: "document",
|
||||
fields: [
|
||||
defineField({
|
||||
name: "title",
|
||||
title: "Title",
|
||||
type: "string",
|
||||
}),
|
||||
defineField({
|
||||
name: "subtitle",
|
||||
title: "Subtitle",
|
||||
type: "string",
|
||||
}),
|
||||
defineField({
|
||||
name: "slug",
|
||||
title: "Slug",
|
||||
type: "slug",
|
||||
options: {
|
||||
source: "title",
|
||||
maxLength: 96,
|
||||
},
|
||||
}),
|
||||
defineField({
|
||||
name: "author",
|
||||
title: "Author",
|
||||
type: "reference",
|
||||
to: { type: "author" },
|
||||
}),
|
||||
defineField({
|
||||
name: "mainImage",
|
||||
title: "Main image",
|
||||
type: "image",
|
||||
options: {
|
||||
hotspot: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "alt",
|
||||
type: "string",
|
||||
title: "Alternative Text",
|
||||
},
|
||||
],
|
||||
}),
|
||||
defineField({
|
||||
name: "categories",
|
||||
title: "Categories",
|
||||
type: "array",
|
||||
of: [{ type: "reference", to: { type: "category" } }],
|
||||
}),
|
||||
defineField({
|
||||
name: "publishedAt",
|
||||
title: "Published at",
|
||||
type: "datetime",
|
||||
}),
|
||||
defineField({
|
||||
name: "content",
|
||||
title: "Body",
|
||||
type: "markdown",
|
||||
options: {
|
||||
imageUrl: (imageAsset) => `${imageAsset.url}`,
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
preview: {
|
||||
select: {
|
||||
title: "title",
|
||||
author: "author.name",
|
||||
media: "mainImage",
|
||||
},
|
||||
prepare(selection) {
|
||||
const { author } = selection;
|
||||
return { ...selection, subtitle: author && `by ${author}` };
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,80 +0,0 @@
|
|||
import { defineField, defineType } from "sanity";
|
||||
|
||||
export default defineType({
|
||||
name: "project",
|
||||
title: "Project",
|
||||
type: "document",
|
||||
fields: [
|
||||
defineField({
|
||||
name: "title",
|
||||
title: "Title",
|
||||
type: "string",
|
||||
}),
|
||||
defineField({
|
||||
name: "subtitle",
|
||||
title: "Subtitle",
|
||||
type: "string",
|
||||
}),
|
||||
defineField({
|
||||
name: "slug",
|
||||
title: "Slug",
|
||||
type: "slug",
|
||||
options: {
|
||||
source: "title",
|
||||
maxLength: 96,
|
||||
},
|
||||
}),
|
||||
defineField({
|
||||
name: "author",
|
||||
title: "Author",
|
||||
type: "reference",
|
||||
to: { type: "author" },
|
||||
}),
|
||||
defineField({
|
||||
name: "mainImage",
|
||||
title: "Main image",
|
||||
type: "image",
|
||||
options: {
|
||||
hotspot: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "alt",
|
||||
type: "string",
|
||||
title: "Alternative Text",
|
||||
},
|
||||
],
|
||||
}),
|
||||
defineField({
|
||||
name: "categories",
|
||||
title: "Categories",
|
||||
type: "array",
|
||||
of: [{ type: "reference", to: { type: "category" } }],
|
||||
}),
|
||||
defineField({
|
||||
name: "publishedAt",
|
||||
title: "Published at",
|
||||
type: "datetime",
|
||||
}),
|
||||
defineField({
|
||||
name: "content",
|
||||
title: "Body",
|
||||
type: "markdown",
|
||||
options: {
|
||||
imageUrl: (imageAsset) => `${imageAsset.url}`,
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
preview: {
|
||||
select: {
|
||||
title: "title",
|
||||
author: "author.name",
|
||||
media: "mainImage",
|
||||
},
|
||||
prepare(selection) {
|
||||
const { author } = selection;
|
||||
return { ...selection, subtitle: author && `by ${author}` };
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,122 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { q } from "groqd";
|
||||
import { client } from "../../../../../sanity/lib/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import CodeBlock from "@/components/Codeblock";
|
||||
import Image from "next/image";
|
||||
import { useNextSanityImage } from "next-sanity-image";
|
||||
|
||||
type BlogPost = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
slug: string;
|
||||
publishedAt: Date;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export default function BlogPostModal({
|
||||
params: { id: slug },
|
||||
}: {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [post, setPost] = React.useState<BlogPost | null>(null);
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
async function getPost() {
|
||||
const { query, schema } = q("*")
|
||||
.filterByType("post")
|
||||
.filter(`slug.current == "${slug}"`)
|
||||
.grab$({
|
||||
title: q.string(),
|
||||
subtitle: q.string(),
|
||||
slug: q.slug("slug"),
|
||||
publishedAt: q.date(),
|
||||
content: q.string(),
|
||||
})
|
||||
.slice(0, 1);
|
||||
|
||||
const post = schema.parse(await client.fetch(query));
|
||||
|
||||
setPost(post[0]);
|
||||
}
|
||||
getPost();
|
||||
}, [slug]);
|
||||
|
||||
return (
|
||||
<Dialog.Root open onOpenChange={handleOpenChange}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="bg-zinc-900 opacity-75 data-[state=open]:animate-overlayShow fixed inset-0" />
|
||||
<Dialog.Content className="data-[state=open]:animate-contentShow overflow-y-scroll fixed top-[50%] left-[50%] w-[90vw] max-h-[85vh] max-w-[50vw] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white dark:bg-zinc-800 px-8 py-12 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none">
|
||||
<Dialog.Title
|
||||
className={cn(
|
||||
"dark:text-white text-indigo-600 m-0 text-6xl font-bold",
|
||||
!post && "bg-gray-500 animate-pulse block w-52 h-5"
|
||||
)}
|
||||
>
|
||||
{post?.title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description
|
||||
className={cn(
|
||||
"dark:text-white text-indigo-500 font-semibold mt-[10px] mb-5 text-2xl leading-normal",
|
||||
!post && "bg-gray-500 animate-pulse block w-72 h-5"
|
||||
)}
|
||||
>
|
||||
{post?.subtitle}
|
||||
</Dialog.Description>
|
||||
|
||||
<article className="prose dark:prose-invert prose-zinc max-w-none lg:prose-xl">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
components={
|
||||
{
|
||||
code: CodeBlock,
|
||||
img: ({ ...props }) => {
|
||||
const { width, height } = props.src.match(
|
||||
/(?<width>\d+)x(?<height>\d+)/
|
||||
).groups;
|
||||
return (
|
||||
<Image
|
||||
src={props.src}
|
||||
alt={props.alt}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
},
|
||||
} as any
|
||||
}
|
||||
>
|
||||
{post?.content || ""}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
|
||||
<Dialog.Close asChild>
|
||||
<button
|
||||
className="text-violet11 hover:bg-violet4 focus:shadow-violet7 absolute top-12 right-8 inline-flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-full focus:shadow-[0_0_0_2px] focus:outline-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Cross2Icon />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export default function Default() {
|
||||
return null;
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { q } from "groqd";
|
||||
import { client } from "../../../../../sanity/lib/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import CodeBlock from "@/components/Codeblock";
|
||||
import Image from "next/image";
|
||||
import { useNextSanityImage } from "next-sanity-image";
|
||||
|
||||
type Project = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
slug: string;
|
||||
publishedAt: Date;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export default function ProjectModal({
|
||||
params: { id: slug },
|
||||
}: {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [project, setProject] = React.useState<Project | null>(null);
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
async function getProject() {
|
||||
const { query: projectQuery, schema: projectSchema } = q("*")
|
||||
.filterByType("project")
|
||||
.filter(`slug.current == "${slug}"`)
|
||||
.grab$({
|
||||
title: q.string(),
|
||||
subtitle: q.string(),
|
||||
slug: q.slug("slug"),
|
||||
publishedAt: q.date(),
|
||||
content: q.string(),
|
||||
})
|
||||
.slice(0, 1);
|
||||
|
||||
const project = projectSchema.parse(await client.fetch(projectQuery));
|
||||
|
||||
setProject(project[0]);
|
||||
}
|
||||
getProject();
|
||||
}, [slug]);
|
||||
|
||||
return (
|
||||
<Dialog.Root open onOpenChange={handleOpenChange}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="bg-zinc-900 opacity-75 data-[state=open]:animate-overlayShow fixed inset-0" />
|
||||
<Dialog.Content className="data-[state=open]:animate-contentShow overflow-y-scroll fixed top-[50%] left-[50%] w-[90vw] max-h-[85vh] md:max-w-[50vw] max-w-[90vw] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white dark:bg-zinc-800 px-8 py-12 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none">
|
||||
<Dialog.Title
|
||||
className={cn(
|
||||
"dark:text-white text-indigo-600 m-0 text-6xl font-bold",
|
||||
!project && "bg-gray-500 animate-pulse block w-52 h-5"
|
||||
)}
|
||||
>
|
||||
{project?.title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description
|
||||
className={cn(
|
||||
"dark:text-white text-indigo-500 font-semibold mt-[10px] mb-5 text-2xl leading-normal",
|
||||
!project && "bg-gray-500 animate-pulse block w-72 h-5"
|
||||
)}
|
||||
>
|
||||
{project?.subtitle}
|
||||
</Dialog.Description>
|
||||
|
||||
<article className="prose dark:prose-invert prose-zinc max-w-none lg:prose-xl">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
components={
|
||||
{
|
||||
code: CodeBlock,
|
||||
img: ({ ...props }) => {
|
||||
const { width, height } = props.src.match(
|
||||
/(?<width>\d+)x(?<height>\d+)/
|
||||
).groups;
|
||||
return (
|
||||
<Image
|
||||
src={props.src}
|
||||
alt={props.alt}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
},
|
||||
} as any
|
||||
}
|
||||
>
|
||||
{project?.content || ""}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
|
||||
<Dialog.Close asChild>
|
||||
<button
|
||||
className="text-violet11 hover:bg-violet4 focus:shadow-violet7 absolute top-12 right-8 inline-flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-full focus:shadow-[0_0_0_2px] focus:outline-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Cross2Icon />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export default function Default() {
|
||||
return null;
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import Logo from "@/components/Logo";
|
||||
import SidecardList from "@/components/SidecardList";
|
||||
import { IdCardIcon } from "@radix-ui/react-icons";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<div className="grid grid-cols-5 gap-4 px-4 mx-auto text-black max-w-7xl dark:text-white">
|
||||
<div className="col-span-4 space-y-3">
|
||||
<h1 className="text-4xl font-black md:text-8xl">
|
||||
Hey! I'm Jack Merrill.
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl">
|
||||
I'm a software engineer, designer, and student from the United
|
||||
States. I'm working to bring accessible designs to the masses.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid order-last w-full h-full grid-cols-2 px-8 py-4 rounded-md lg:order-none lg:space-y-12 gap-x-2 lg:row-span-3 lg:grid-cols-1 lg:col-span-1 col-span-full bg-violet-500">
|
||||
<div className="flex justify-center lg:w-full">
|
||||
<div className="h-auto p-8 border-4 border-white aspect-square w-fit max-h-48 lg:p-12 md:p-10 sm:p-6 rounded-xl">
|
||||
<Logo />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SidecardList />
|
||||
</div>
|
||||
|
||||
<div className="col-span-4 space-y-3">
|
||||
<h2 className="text-3xl font-black md:text-6xl">About Me</h2>
|
||||
|
||||
<p className="text-xl md:text-2xl">
|
||||
I'm a Division II (sophomore) student at Hampshire College,
|
||||
studying interaction design. I'm also a full-stack web developer
|
||||
at{" "}
|
||||
<Link
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
href="https://merch.co"
|
||||
className="font-bold text-violet-500"
|
||||
>
|
||||
Merch
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
import { q } from "groqd";
|
||||
import { client } from "../../../../sanity/lib/client";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import CodeBlock from "@/components/Codeblock";
|
||||
import Image from "next/image";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
export default async function Page({
|
||||
params: { id: slug },
|
||||
}: {
|
||||
params: { id: string };
|
||||
}) {
|
||||
const { query, schema } = q("*")
|
||||
.filterByType("post")
|
||||
.filter(`slug.current == "${slug}"`)
|
||||
.grab$({
|
||||
title: q.string(),
|
||||
subtitle: q.string(),
|
||||
slug: q.slug("slug"),
|
||||
publishedAt: q.date(),
|
||||
content: q.string(),
|
||||
mainImage: q("mainImage").grabOne$("asset->url", q.string().optional()),
|
||||
})
|
||||
.slice(0, 1);
|
||||
|
||||
const post = schema.parse(await client.fetch(query))[0];
|
||||
|
||||
const r = post.mainImage?.match(/(?<width>\d+)x(?<height>\d+)/);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen py-2">
|
||||
<div className="flex items-center justify-center w-full py-24">
|
||||
{/* the mainimage with the text on top of it */}
|
||||
<div className="relative w-full h-96">
|
||||
<div className="absolute inset-0 flex items-center justify-center w-full h-full bg-black bg-opacity-50">
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<h1 className="text-4xl font-bold text-center text-white">
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
<h2 className="text-2xl font-semibold text-center text-white">
|
||||
{post.subtitle}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
{post.mainImage && (
|
||||
<Image
|
||||
className="object-cover w-full h-full"
|
||||
src={post.mainImage}
|
||||
alt={post.title}
|
||||
width={parseInt(r?.groups?.width ?? "400")}
|
||||
height={parseInt(r?.groups?.height ?? "400")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* the content */}
|
||||
<article className="mx-auto prose dark:prose-invert prose-zinc max-w-7xl lg:prose-xl">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
components={
|
||||
{
|
||||
code: CodeBlock,
|
||||
img: ({ ...props }) => {
|
||||
const { width, height } = props.src.match(
|
||||
/(?<width>\d+)x(?<height>\d+)/
|
||||
).groups;
|
||||
return (
|
||||
<Image
|
||||
src={props.src}
|
||||
alt={props.alt}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
},
|
||||
} as any
|
||||
}
|
||||
>
|
||||
{post?.content || ""}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const { query, schema } = q("*")
|
||||
.filterByType("post")
|
||||
.grabOne$("slug.current", q.string());
|
||||
|
||||
const slugs = schema.parse(await client.fetch(query));
|
||||
|
||||
return slugs.map((slug) => ({ params: { id: slug } }));
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
import { q } from "groqd";
|
||||
import { client } from "../../../sanity/lib/client";
|
||||
import Twemoji from "@/components/Twemoji";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Page() {
|
||||
const { query, schema } = q("*")
|
||||
.filterByType("post")
|
||||
.order("publishedAt desc")
|
||||
.grab$({
|
||||
title: q.string(),
|
||||
subtitle: q.string(),
|
||||
slug: q.slug("slug"),
|
||||
publishedAt: q.date(),
|
||||
mainImage: q("mainImage").grabOne$("asset->url", q.string().optional()),
|
||||
categories: q("categories")
|
||||
.filter()
|
||||
.deref()
|
||||
.grabOne$("title", q.string())
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
const posts = schema.parse(await client.fetch(query));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section className="flex items-center px-6 py-48 mx-auto h-2/3 max-w-7xl">
|
||||
<div className="space-y-4">
|
||||
<h1 className="flex items-center text-6xl font-bold gap-x-4">
|
||||
<Twemoji emoji="📰" ext="svg" /> Blog
|
||||
</h1>
|
||||
<h2 className="text-2xl font-semibold">
|
||||
My thoughts on web, tech, and life. (other things too)
|
||||
</h2>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="dark:bg-zinc-900">
|
||||
<section className="grid grid-cols-2 gap-4 py-8 mx-auto max-w-7xl">
|
||||
{posts.map((post) => {
|
||||
const r = post.mainImage?.match(/(?<width>\d+)x(?<height>\d+)/);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={post.slug}
|
||||
className="flex flex-col items-center justify-center pb-4 space-y-4 overflow-hidden transition-all duration-150 rounded-md dark:bg-zinc-800 hover:scale-105"
|
||||
href={`/blog/${post.slug}`}
|
||||
>
|
||||
{post.mainImage && (
|
||||
<Image
|
||||
src={post.mainImage}
|
||||
alt={post.title}
|
||||
width={parseInt(r?.groups?.width ?? "400")}
|
||||
height={parseInt(r?.groups?.height ?? "400")}
|
||||
/>
|
||||
)}
|
||||
<h3 className="text-xl font-semibold">{post.title}</h3>
|
||||
<p className="text-lg">{post.subtitle}</p>
|
||||
|
||||
{post.categories && post.categories.length > 0 && (
|
||||
<p className="text-md text-zinc-400">
|
||||
Categories:
|
||||
{post.categories.map((category) => (
|
||||
<span
|
||||
key={category}
|
||||
className="px-2 py-1 ml-2 text-sm font-semibold text-white bg-indigo-500 rounded-md"
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<time
|
||||
className="text-sm text-gray-500"
|
||||
dateTime={post.publishedAt.toISOString()}
|
||||
>
|
||||
Published at{" "}
|
||||
{new Date(post.publishedAt).toLocaleDateString()}
|
||||
</time>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Before Width: | Height: | Size: 25 KiB |
|
@ -1,153 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 100% 50%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 215 20.2% 65.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71% 4%;
|
||||
--foreground: 213 31% 91%;
|
||||
|
||||
--muted: 223 47% 11%;
|
||||
--muted-foreground: 215.4 16.3% 56.9%;
|
||||
|
||||
--popover: 224 71% 4%;
|
||||
--popover-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--card: 224 71% 4%;
|
||||
--card-foreground: 213 31% 91%;
|
||||
|
||||
--border: 216 34% 17%;
|
||||
--input: 216 34% 17%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 1.2%;
|
||||
|
||||
--secondary: 222.2 47.4% 11.2%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--accent: 216 34% 17%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 63% 31%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 216 34% 17%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
.rainbow-btn {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rainbow-btn::before {
|
||||
width: 100%;
|
||||
height: 175%;
|
||||
content: "Contact";
|
||||
padding: 0.5em 0.75em;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -40%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-stroke: 12px transparent;
|
||||
font-family: Nunito, system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu,
|
||||
Cantarell, "Noto Sans", sans-serif;
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
font-weight: 800;
|
||||
filter: blur(17px);
|
||||
z-index: -1;
|
||||
animation: spin 10s linear infinite;
|
||||
background: conic-gradient(
|
||||
from 125deg at 50% 50%,
|
||||
#ff4e4eff 1%,
|
||||
#f3a43fff 14%,
|
||||
#f3f23fff 27%,
|
||||
#63f33fff 39%,
|
||||
#3fb9f3ff 52%,
|
||||
#a03ff3ff 66%,
|
||||
#fc00b4ff 80%,
|
||||
#ff4e4eff 96%
|
||||
);
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* This route is responsible for the built-in authoring environment using Sanity Studio.
|
||||
* All routes under your studio path is handled by this file using Next.js' catch-all routes:
|
||||
* https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes
|
||||
*
|
||||
* You can learn more about the next-sanity package here:
|
||||
* https://github.com/sanity-io/next-sanity
|
||||
*/
|
||||
|
||||
import { NextStudio } from "next-sanity/studio";
|
||||
import config from "../../../../../sanity.config";
|
||||
|
||||
const isDev =
|
||||
process.env.NODE_ENV === "development" ||
|
||||
!process.env.NODE_ENV ||
|
||||
process.env.NEXT_PUBLIC_ENV === "development" ||
|
||||
!process.env.NEXT_PUBLIC_ENV;
|
||||
|
||||
export default function StudioPage() {
|
||||
return isDev ? <NextStudio config={config} /> : null;
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
import "./globals.css";
|
||||
import { Inter } from "next/font/google";
|
||||
import Background from "@/components/Background";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import { client } from "../../sanity/lib/client";
|
||||
import type ProjectType from "../../sanity/schemas/project";
|
||||
import { q } from "groqd";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
GitHubLogoIcon,
|
||||
LinkedInLogoIcon,
|
||||
TwitterLogoIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import Twemoji from "@/components/Twemoji";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata = {
|
||||
title: "Jack Merrill",
|
||||
description:
|
||||
"Web designer and developer working to bring accessible designs to the masses.",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
project,
|
||||
blogpost,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
project?: React.ReactNode;
|
||||
blogpost?: React.ReactNode;
|
||||
}) {
|
||||
const { query: projectQuery, schema: projectSchema } = q("*")
|
||||
.filterByType("project")
|
||||
.order("publishedAt desc")
|
||||
.grab$({
|
||||
title: q.string(),
|
||||
subtitle: q.string(),
|
||||
slug: q.slug("slug"),
|
||||
publishedAt: q.date(),
|
||||
mainImage: q("mainImage").grabOne$("asset->url", q.string().optional()),
|
||||
})
|
||||
.slice(0, 2);
|
||||
|
||||
const { query: blogQuery, schema: blogSchema } = q("*")
|
||||
.filterByType("post")
|
||||
.slice(0, 3)
|
||||
.order("publishedAt desc")
|
||||
.grab$({
|
||||
title: q.string(),
|
||||
slug: q.slug("slug"),
|
||||
publishedAt: q.date(),
|
||||
categories: q("categories")
|
||||
.filter()
|
||||
.deref()
|
||||
.grabOne$("title", q.string())
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
const latestThreeProjects = projectSchema.parse(
|
||||
await client.fetch(projectQuery)
|
||||
);
|
||||
|
||||
const latestThreeBlogPosts = blogSchema.parse(await client.fetch(blogQuery));
|
||||
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<body
|
||||
className={`${inter.className} min-h-[100vh] flex justify-between flex-col bg-white heropattern-wiggle-indigo-100 dark:bg-zinc-900 dark:heropattern-wiggle-zinc-800`}
|
||||
>
|
||||
<Navbar
|
||||
projects={latestThreeProjects}
|
||||
blogPosts={latestThreeBlogPosts}
|
||||
/>
|
||||
|
||||
<main className="w-full min-h-full space-y-6">{children}</main>
|
||||
|
||||
<footer className="flex justify-center w-full py-8 dark:bg-zinc-900 bg-zinc-300">
|
||||
<div className="flex flex-col items-center w-full px-4 space-y-2 md:flex-row md:justify-between max-w-7xl">
|
||||
<p className="flex items-center space-x-1 text-lg text-center text-black dark:text-zinc-100">
|
||||
Made with
|
||||
<Twemoji
|
||||
emoji="❤️"
|
||||
className="w-5 h-5 mx-1 text-red-500 hover:animate-heartbeat"
|
||||
ext="svg"
|
||||
/>
|
||||
by Jack Merrill
|
||||
</p>
|
||||
|
||||
<p className="text-xs font-light text-zinc-700">
|
||||
Build{" "}
|
||||
{process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA
|
||||
? process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA.slice(0, 7)
|
||||
: "dev"}
|
||||
</p>
|
||||
|
||||
<div className="flex space-x-4 text-black dark:text-zinc-100">
|
||||
<Link
|
||||
href="https://www.linkedin.com/in/jack-merrill-39aa7520b/"
|
||||
target="_blank"
|
||||
>
|
||||
<LinkedInLogoIcon className="w-6 h-6 transition-colors duration-150 cursor-pointer hover:text-[#0a66c2]" />
|
||||
</Link>
|
||||
|
||||
<Link href="https://github.com/jackmerrill" target="_blank">
|
||||
<GitHubLogoIcon className="w-6 h-6 transition-colors duration-150 cursor-pointer hover:text-pink-500" />
|
||||
</Link>
|
||||
|
||||
<Link href="https://twitter.com/jack__merrill" target="_blank">
|
||||
<TwitterLogoIcon className="w-6 h-6 transition-colors duration-150 cursor-pointer hover:text-[#1d9bf0]" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{project}
|
||||
{blogpost}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
112
src/app/page.tsx
|
@ -1,112 +0,0 @@
|
|||
import Background from "@/components/Background";
|
||||
import Button from "@/components/Button";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Twemoji from "@/components/Twemoji";
|
||||
import {
|
||||
CodeIcon,
|
||||
CubeIcon,
|
||||
FrameIcon,
|
||||
HobbyKnifeIcon,
|
||||
MagicWandIcon,
|
||||
ScissorsIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<section className="flex items-center px-6 py-48 mx-auto text-black dark:text-white h-2/3 max-w-7xl">
|
||||
<div className="space-y-4">
|
||||
<h1 className="flex items-center text-6xl font-bold gap-x-4">
|
||||
<Twemoji
|
||||
emoji="👋"
|
||||
ext="svg"
|
||||
className="motion-safe:animate-hand-wave"
|
||||
/>{" "}
|
||||
Hey hey!
|
||||
</h1>
|
||||
<h2 className="text-2xl font-semibold">
|
||||
I'm Jack Merrill, a web designer and developer working to bring
|
||||
accessible designs to the masses.
|
||||
</h2>
|
||||
|
||||
<div className="flex">
|
||||
<Button link href="mailto:contact@jackmerrill.com">
|
||||
Contact me
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 py-28 dark:bg-zinc-900 bg-slate-100">
|
||||
<div className="flex flex-col items-center mx-auto space-y-4 max-w-7xl">
|
||||
<h2 className="text-4xl font-bold">What I do</h2>
|
||||
|
||||
<div className="grid gap-4 text-black md:grid-cols-2 lg:grid-cols-4 dark:text-zinc-100">
|
||||
<div className="grid items-center grid-rows-2 p-4 group gap-y-2 dark:bg-zinc-800 bg-slate-300">
|
||||
<div></div>
|
||||
<div className="space-y-2">
|
||||
<FrameIcon className="w-12 h-12 transition-colors duration-150 group-hover:text-pink-500" />
|
||||
<h3 className="text-2xl font-semibold">UI Design</h3>
|
||||
<p className="text-lg">
|
||||
Designing interfaces and websites that are accessible, usable,
|
||||
and intuitive.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid items-center grid-rows-2 p-4 group gap-y-2 dark:bg-zinc-800 bg-slate-300">
|
||||
<div></div>
|
||||
<div className="space-y-2">
|
||||
<CodeIcon className="w-12 h-12 transition-colors duration-150 group-hover:text-teal-500" />{" "}
|
||||
<h3 className="text-2xl font-semibold">Development</h3>
|
||||
<p className="text-lg">
|
||||
Building websites and software that are fast, responsive, and
|
||||
accessible.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid items-center grid-rows-2 p-4 group gap-y-2 dark:bg-zinc-800 bg-slate-300">
|
||||
<div></div>
|
||||
<div className="space-y-2">
|
||||
<MagicWandIcon className="w-12 h-12 transition-colors duration-150 group-hover:text-indigo-500" />{" "}
|
||||
<h3 className="text-2xl font-semibold">Creative</h3>
|
||||
<p className="text-lg">
|
||||
Creating stunning designs and illustrations that are unique
|
||||
and memorable.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid items-center grid-rows-2 p-4 group gap-y-2 dark:bg-zinc-800 bg-slate-300">
|
||||
<div></div>
|
||||
<div className="space-y-2">
|
||||
<ScissorsIcon className="w-12 h-12 transition-colors duration-150 group-hover:text-green-500" />{" "}
|
||||
<h3 className="text-2xl font-semibold">Other</h3>
|
||||
<p className="text-lg">
|
||||
I'm always looking to learn new things and expand my
|
||||
skillset. Check out my projects for more.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 py-28">
|
||||
<div className="flex flex-col items-center mx-auto space-y-4 text-center max-w-7xl">
|
||||
<h2 className="text-4xl font-bold text-black dark:text-zinc-100">
|
||||
Tell me about your next project
|
||||
</h2>
|
||||
|
||||
<div className="flex">
|
||||
<Button rainbow link href="mailto:contact@jackmerrill.com">
|
||||
Reach out
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
import { q } from "groqd";
|
||||
import { client } from "../../../../sanity/lib/client";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import CodeBlock from "@/components/Codeblock";
|
||||
import Image from "next/image";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
export default async function Page({
|
||||
params: { id },
|
||||
}: {
|
||||
params: { id: string };
|
||||
}) {
|
||||
const { query: projectQuery, schema: projectSchema } = q("*")
|
||||
.filterByType("project")
|
||||
.filter(`slug.current == "${id}"`)
|
||||
.grab$({
|
||||
title: q.string(),
|
||||
subtitle: q.string(),
|
||||
slug: q.slug("slug"),
|
||||
publishedAt: q.date(),
|
||||
content: q.string(),
|
||||
mainImage: q("mainImage").grabOne$("asset->url", q.string().optional()),
|
||||
});
|
||||
|
||||
const project = projectSchema.parse(await client.fetch(projectQuery))[0];
|
||||
|
||||
const r = project.mainImage?.match(/(?<width>\d+)x(?<height>\d+)/);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen py-2">
|
||||
<div className="flex items-center justify-center w-full py-24">
|
||||
{/* the mainimage with the text on top of it */}
|
||||
<div className="relative w-full h-96">
|
||||
<div className="absolute inset-0 flex items-center justify-center w-full h-full bg-black bg-opacity-50">
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<h1 className="text-4xl font-bold text-center text-white">
|
||||
{project.title}
|
||||
</h1>
|
||||
|
||||
<h2 className="text-2xl font-semibold text-center text-white">
|
||||
{project.subtitle}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
{project.mainImage && (
|
||||
<Image
|
||||
className="object-cover w-full h-full"
|
||||
src={project.mainImage}
|
||||
alt={project.title}
|
||||
width={parseInt(r?.groups?.width ?? "400")}
|
||||
height={parseInt(r?.groups?.height ?? "400")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* the content */}
|
||||
<article className="px-4 pb-12 mx-auto prose dark:prose-invert prose-zinc max-w-7xl lg:prose-xl">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
components={
|
||||
{
|
||||
code: CodeBlock,
|
||||
img: ({ ...props }) => {
|
||||
const { width, height } = props.src.match(
|
||||
/(?<width>\d+)x(?<height>\d+)/
|
||||
).groups;
|
||||
return (
|
||||
<Image
|
||||
src={props.src}
|
||||
alt={props.alt}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
},
|
||||
} as any
|
||||
}
|
||||
>
|
||||
{project?.content || ""}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const { query: projectQuery, schema: projectSchema } = q("*")
|
||||
.filterByType("project")
|
||||
.grabOne$("slug.current", q.string());
|
||||
|
||||
const projects = projectSchema.parse(await client.fetch(projectQuery));
|
||||
|
||||
return projects.map((project) => ({
|
||||
params: {
|
||||
slug: project,
|
||||
},
|
||||
}));
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
import { q } from "groqd";
|
||||
import { client } from "../../../sanity/lib/client";
|
||||
import Twemoji from "@/components/Twemoji";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Page() {
|
||||
const { query: projectQuery, schema: projectSchema } = q("*")
|
||||
.filterByType("project")
|
||||
.order("publishedAt desc")
|
||||
.grab$({
|
||||
title: q.string(),
|
||||
subtitle: q.string(),
|
||||
slug: q.slug("slug"),
|
||||
publishedAt: q.date(),
|
||||
mainImage: q("mainImage").grabOne$("asset->url", q.string().optional()),
|
||||
categories: q("categories")
|
||||
.filter()
|
||||
.deref()
|
||||
.grabOne$("title", q.string())
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
const projects = projectSchema.parse(await client.fetch(projectQuery));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section className="flex items-center px-6 py-48 mx-auto text-black dark:text-white h-2/3 max-w-7xl">
|
||||
<div className="space-y-4">
|
||||
<h1 className="flex items-center text-6xl font-bold gap-x-4">
|
||||
<Twemoji emoji="🛠" ext="svg" /> Projects
|
||||
</h1>
|
||||
<h2 className="text-2xl font-semibold">
|
||||
The weird and wonderful things I work on.
|
||||
</h2>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="dark:bg-zinc-900 bg-slate-200">
|
||||
<section className="grid gap-4 px-4 py-8 mx-auto md:grid-cols-4 max-w-7xl">
|
||||
{projects.map((project) => {
|
||||
const r = project.mainImage?.match(/(?<width>\d+)x(?<height>\d+)/);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={project.slug}
|
||||
className="flex flex-col items-center justify-center h-full p-6 space-y-4 text-black transition-all duration-150 rounded-md dark:text-white dark:bg-zinc-800 bg-slate-300 hover:scale-105"
|
||||
href={`/projects/${project.slug}`}
|
||||
>
|
||||
<div>
|
||||
{project.mainImage && (
|
||||
<Image
|
||||
src={project.mainImage}
|
||||
alt={project.title}
|
||||
width={parseInt(r?.groups?.width ?? "400")}
|
||||
height={parseInt(r?.groups?.height ?? "400")}
|
||||
/>
|
||||
)}
|
||||
<h3 className="text-xl font-semibold">{project.title}</h3>
|
||||
<p className="text-lg">{project.subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow">
|
||||
{project.categories && project.categories.length > 0 && (
|
||||
<div className="flex flex-wrap flex-grow gap-1 text-md dark:text-zinc-400 text-slate-600">
|
||||
<span className="w-full">Categories:</span>
|
||||
{project.categories.map((category) => (
|
||||
<span
|
||||
key={category}
|
||||
className="px-2 py-1 text-sm font-semibold text-white bg-indigo-500 rounded-md"
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center mt-auto space-x-2">
|
||||
<time
|
||||
className="text-sm text-gray-500"
|
||||
dateTime={project.publishedAt.toISOString()}
|
||||
>
|
||||
Published at{" "}
|
||||
{new Date(project.publishedAt).toLocaleDateString()}
|
||||
</time>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
BIN
src/assets/img/DJI_0565.JPG
Normal file
After Width: | Height: | Size: 2.9 MiB |
BIN
src/assets/img/DJI_0605.JPG
Normal file
After Width: | Height: | Size: 2.6 MiB |
BIN
src/assets/img/IMG_0196.JPG
Normal file
After Width: | Height: | Size: 541 KiB |
BIN
src/assets/img/IMG_3942.JPG
Normal file
After Width: | Height: | Size: 602 KiB |
BIN
src/assets/memoji.png
Normal file
After Width: | Height: | Size: 84 KiB |
66
src/components/AR.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { Interactive, XR, ARButton, Controllers } from "@react-three/xr";
|
||||
import { Text } from "@react-three/drei";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { useState, Suspense } from "react";
|
||||
|
||||
function Box({ color, size, scale, children, ...rest }: any) {
|
||||
return (
|
||||
<mesh scale={scale} {...rest}>
|
||||
<boxBufferGeometry args={size} />
|
||||
<meshPhongMaterial color={color} />
|
||||
{children}
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
function Button(props: any) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [color, setColor] = useState<any>("blue");
|
||||
|
||||
const onSelect = () => {
|
||||
setColor((Math.random() * 0xffffff) | 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Interactive
|
||||
onHover={() => setHover(true)}
|
||||
onBlur={() => setHover(false)}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
<Box
|
||||
color={color}
|
||||
scale={hover ? [0.6, 0.6, 0.6] : [0.5, 0.5, 0.5]}
|
||||
size={[0.4, 0.1, 0.1]}
|
||||
{...props}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<Text
|
||||
position={[0, 0, 0.06]}
|
||||
fontSize={0.05}
|
||||
color="#000"
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
>
|
||||
Hello react-xr!
|
||||
</Text>
|
||||
</Suspense>
|
||||
</Box>
|
||||
</Interactive>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AR() {
|
||||
return (
|
||||
<>
|
||||
<ARButton />
|
||||
<Canvas>
|
||||
<XR referenceSpace="local">
|
||||
<ambientLight />
|
||||
<pointLight position={[10, 10, 10]} />
|
||||
<Button position={[0, 0.1, -0.2]} />
|
||||
<Controllers />
|
||||
</XR>
|
||||
</Canvas>
|
||||
</>
|
||||
);
|
||||
}
|