Compare commits

..

No commits in common. "master" and "v2" have entirely different histories.
master ... v2

136 changed files with 29435 additions and 5338 deletions

View File

@ -1,10 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,json,yml}]
charset = utf-8
indent_style = space
indent_size = 2

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

4
.gitattributes vendored
View File

@ -1,4 +0,0 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated

48
.gitignore vendored
View File

@ -1,22 +1,44 @@
# build output
dist/
# generated types
.astro/
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules/
/node_modules
/.pnp
.pnp.js
# logs
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# sanity
/dist
.sanity/
# env
.env
.env.local
.env.development.local

View File

@ -1,4 +0,0 @@
{
"tabWidth": 2,
"useTabs": false
}

1
.vercelignore Normal file
View File

@ -0,0 +1 @@
/src/app/internal

View File

@ -1,4 +0,0 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored
View File

@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

Binary file not shown.

View File

@ -1 +0,0 @@
nodeLinker: node-modules

View File

@ -1,3 +1,34 @@
# jackmerrill.com
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).
This is my website! Written using Astro (and some React), styled with TailwindCSS, designed with Figma, and deployed with Vercel.
## 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.

View File

@ -1,28 +0,0 @@
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(),
// },
});

8
next.config.js Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ["cdn.sanity.io"],
},
};
module.exports = nextConfig;

19845
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +1,58 @@
{
"name": "jackmerrill-com",
"type": "module",
"version": "0.0.1",
"name": "jackmerrill.com",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@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"
"@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"
},
"devDependencies": {
"sass": "^1.69.5",
"typescript": "^5.5.4"
"@types/react-syntax-highlighter": "^15.5.7",
"@types/twemoji": "^13.1.2"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,182 +0,0 @@
/**
* @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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 629 B

10
sanity.cli.ts Normal file
View File

@ -0,0 +1,10 @@
/**
* 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 } })

27
sanity.config.ts Normal file
View File

@ -0,0 +1,27 @@
/**
* This configuration is used to for the Sanity Studio thats 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(),
],
});

22
sanity/env.ts Normal file
View File

@ -0,0 +1,22 @@
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;
}

10
sanity/lib/client.ts Normal file
View File

@ -0,0 +1,10 @@
import { createClient } from 'next-sanity'
import { apiVersion, dataset, projectId, useCdn } from '../env'
export const client = createClient({
apiVersion,
dataset,
projectId,
useCdn,
})

13
sanity/lib/image.ts Normal file
View File

@ -0,0 +1,13 @@
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')
}

11
sanity/schema.ts Normal file
View File

@ -0,0 +1,11 @@
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],
};

57
sanity/schemas/author.ts Normal file
View File

@ -0,0 +1,57 @@
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",
},
},
});

View File

@ -0,0 +1,75 @@
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',
}
]
}),
],
})

View File

@ -0,0 +1,19 @@
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',
}),
],
})

80
sanity/schemas/post.ts Normal file
View File

@ -0,0 +1,80 @@
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}` };
},
},
});

80
sanity/schemas/project.ts Normal file
View File

@ -0,0 +1,80 @@
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}` };
},
},
});

View File

@ -0,0 +1,122 @@
"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>
);
}

View File

@ -0,0 +1,3 @@
export default function Default() {
return null;
}

View File

@ -0,0 +1,122 @@
"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>
);
}

View File

@ -0,0 +1,3 @@
export default function Default() {
return null;
}

51
src/app/about/page.tsx Normal file
View File

@ -0,0 +1,51 @@
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&apos;m Jack Merrill.
</h1>
<p className="text-xl md:text-2xl">
I&apos;m a software engineer, designer, and student from the United
States. I&apos;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&apos;m a Division II (sophomore) student at Hampshire College,
studying interaction design. I&apos;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>
);
}

View File

@ -0,0 +1,98 @@
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 } }));
}

91
src/app/blog/page.tsx Normal file
View File

@ -0,0 +1,91 @@
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>
);
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

153
src/app/globals.css Normal file
View File

@ -0,0 +1,153 @@
@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);
}
}

View File

@ -0,0 +1,23 @@
"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;
}

121
src/app/layout.tsx Normal file
View File

@ -0,0 +1,121 @@
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 Normal file
View File

@ -0,0 +1,112 @@
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&apos;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&apos;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>
</>
);
}

View File

@ -0,0 +1,101 @@
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,
},
}));
}

95
src/app/projects/page.tsx Normal file
View File

@ -0,0 +1,95 @@
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>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

View File

@ -1,66 +0,0 @@
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>
</>
);
}

Some files were not shown because too many files have changed in this diff Show More