@@ -0,0 +1 @@ | |||||
.idea/ |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"extends": "next/core-web-vitals" | |||||
} |
@@ -0,0 +1,35 @@ | |||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | |||||
# dependencies | |||||
/node_modules | |||||
/.pnp | |||||
.pnp.js | |||||
# testing | |||||
/coverage | |||||
# next.js | |||||
/.next/ | |||||
/out/ | |||||
# production | |||||
/build | |||||
# misc | |||||
.DS_Store | |||||
*.pem | |||||
# debug | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
# local env files | |||||
.env*.local | |||||
# vercel | |||||
.vercel | |||||
# typescript | |||||
*.tsbuildinfo | |||||
next-env.d.ts |
@@ -0,0 +1,38 @@ | |||||
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). | |||||
## 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 `pages/index.tsx`. The page auto-updates as you edit the file. | |||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. | |||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. | |||||
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. |
@@ -0,0 +1,3 @@ | |||||
[install.scopes] | |||||
"@tesseract-design" = "http://localhost:4873/" | |||||
"@modal-sh" = "http://localhost:4873/" |
@@ -0,0 +1,6 @@ | |||||
/** @type {import('next').NextConfig} */ | |||||
const nextConfig = { | |||||
reactStrictMode: true, | |||||
} | |||||
module.exports = nextConfig |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"name": "web", | |||||
"version": "0.1.0", | |||||
"private": true, | |||||
"scripts": { | |||||
"dev": "next dev", | |||||
"build": "next build", | |||||
"start": "next start", | |||||
"lint": "next lint" | |||||
}, | |||||
"dependencies": { | |||||
"@tesseract-design/viewfinder-base": "0.0.1", | |||||
"@tesseract-design/viewfinder-react": "0.0.1", | |||||
"@tesseract-design/web-action-react": "^0.2.0", | |||||
"@tesseract-design/web-formatted-react": "^0.2.0", | |||||
"@tesseract-design/web-freeform-react": "^0.2.0", | |||||
"@tesseract-design/web-navigation-react": "^0.2.0", | |||||
"@theoryofnekomata/formxtra": "^1.0.3", | |||||
"@types/node": "20.6.0", | |||||
"@types/react": "18.2.21", | |||||
"@types/react-dom": "18.2.7", | |||||
"autoprefixer": "10.4.15", | |||||
"clsx": "^2.0.0", | |||||
"eslint": "8.49.0", | |||||
"eslint-config-next": "13.4.19", | |||||
"next": "13.4.19", | |||||
"postcss": "8.4.29", | |||||
"react": "18.2.0", | |||||
"react-dom": "18.2.0", | |||||
"tailwindcss": "3.3.3", | |||||
"typescript": "5.2.2" | |||||
} | |||||
} |
@@ -0,0 +1,6 @@ | |||||
module.exports = { | |||||
plugins: { | |||||
tailwindcss: {}, | |||||
autoprefixer: {}, | |||||
}, | |||||
} |
@@ -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> |
@@ -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> |
@@ -0,0 +1,75 @@ | |||||
import * as React from 'react'; | |||||
export interface BackgroundGridProps { | |||||
level?: number; | |||||
images: string[]; | |||||
parentKey?: React.Key; | |||||
} | |||||
export const BackgroundGrid: React.FC<BackgroundGridProps> = ({ | |||||
level = 0, | |||||
images, | |||||
parentKey = level, | |||||
}) => { | |||||
const [values, setValues] = React.useState<(string | null)[]>(); | |||||
React.useEffect(() => { | |||||
const newValues = []; | |||||
for (let i = 0; i < 4; i += 1) { | |||||
if (level < 1) { | |||||
newValues.push(null); | |||||
continue; | |||||
} | |||||
if (level < 3) { | |||||
const hasInnerGrid = Math.floor(Math.random() * 2) === 1; | |||||
if (hasInnerGrid) { | |||||
newValues.push(null); | |||||
continue; | |||||
} | |||||
} | |||||
const randomImage = images[Math.floor(Math.random() * images.length)]; | |||||
newValues.push(randomImage); | |||||
} | |||||
setValues(newValues); | |||||
}, []); | |||||
return ( | |||||
<div className="grid grid-cols-2 grid-rows-2 w-full h-full"> | |||||
{values?.map((value, index) => { | |||||
if (value === null) { | |||||
return ( | |||||
<BackgroundGrid | |||||
images={images} | |||||
level={level + 1} | |||||
key={`${parentKey}:${index}`} | |||||
/> | |||||
); | |||||
// return ( | |||||
// level < 3 | |||||
// ? ( | |||||
// <BackgroundGrid | |||||
// images={images} | |||||
// level={level + 1} | |||||
// key={index} | |||||
// /> | |||||
// ) | |||||
// : <div /> | |||||
// ); | |||||
} | |||||
return ( | |||||
<div | |||||
key={`${parentKey}:${index}`} | |||||
className="bg-cover bg-center border border-shade" | |||||
style={{backgroundImage: `url("${value}")`}} | |||||
/> | |||||
); | |||||
})} | |||||
</div> | |||||
); | |||||
} |
@@ -0,0 +1,7 @@ | |||||
import * as React from 'react'; | |||||
export const Brand = () => ( | |||||
<span className="rounded-[0.25em] lowercase align-middle font-headings bg-positive text-negative px-1.5 font-semibold -mx-2 before:content-['@']"> | |||||
TheoryOfNekomata | |||||
</span> | |||||
); |
@@ -0,0 +1,124 @@ | |||||
import * as React from 'react'; | |||||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||||
import * as WebNavigationReact from '@tesseract-design/web-navigation-react'; | |||||
import * as WebFreeformReact from '@tesseract-design/web-freeform-react'; | |||||
import * as WebFormattedReact from '@tesseract-design/web-formatted-react'; | |||||
import * as WebActionReact from '@tesseract-design/web-action-react'; | |||||
export interface ContactCtaBannerProps { | |||||
onSubmit?: React.FormEventHandler<HTMLFormElement>; | |||||
} | |||||
export const ContactCtaBanner: React.FC<ContactCtaBannerProps> = ({ | |||||
onSubmit, | |||||
}) => { | |||||
const [visible, setVisible] = React.useState<boolean>(); | |||||
const autofocusRef = React.useRef<HTMLInputElement>(null); | |||||
const messageRef = React.useRef<HTMLTextAreaElement>(null); | |||||
const openContactForm: React.MouseEventHandler<HTMLAnchorElement> = (e) => { | |||||
e.preventDefault(); | |||||
setVisible(true); | |||||
window.setTimeout(() => { | |||||
autofocusRef.current?.focus(); | |||||
if (messageRef.current && messageRef.current.style.height === '0px') { | |||||
messageRef.current.style.height = '12rem'; | |||||
} | |||||
}); | |||||
}; | |||||
React.useEffect(() => { | |||||
setVisible(false); | |||||
}, []); | |||||
return ( | |||||
<div className="pt-8 sm:pt-16 pb-8 sm:pb-16 flex items-center relative"> | |||||
<div | |||||
className="absolute top-0 left-0 w-full h-full bg-shade opacity-75" | |||||
/> | |||||
<Layouts.Basic.ContentContainer | |||||
span="wide" | |||||
className="relative" | |||||
> | |||||
<div className="flex flex-wrap flex-row items-center sm:justify-between gap-6 sm:gap-12"> | |||||
<p className="m-0"> | |||||
<strong | |||||
className="font-extralight font-headings text-5xl lowercase" | |||||
> | |||||
Get in touch. | |||||
</strong> | |||||
</p> | |||||
<p className="m-0"> | |||||
Your message will be received via email. | |||||
</p> | |||||
<div | |||||
className={`grow sm:grow-0 text-right ${visible ? 'hidden' : ''}`} | |||||
> | |||||
<WebNavigationReact.LinkButton | |||||
component="a" | |||||
href="/contact" | |||||
onClick={openContactForm} | |||||
> | |||||
Open Form | |||||
</WebNavigationReact.LinkButton> | |||||
</div> | |||||
</div> | |||||
{visible !== false && ( | |||||
<form | |||||
className="mt-8" | |||||
aria-label="Contact Form" | |||||
onSubmit={onSubmit} | |||||
method="post" | |||||
action="/a/contact" | |||||
> | |||||
<fieldset className="contents"> | |||||
<legend className="sr-only"> | |||||
Contact Form | |||||
</legend> | |||||
<div | |||||
className="flex flex-col xs:grid xs:grid-cols-2 gap-4" | |||||
> | |||||
<div> | |||||
<WebFreeformReact.TextInput | |||||
label="Name" | |||||
name="name" | |||||
block | |||||
border | |||||
ref={autofocusRef} | |||||
/> | |||||
</div> | |||||
<div> | |||||
<WebFormattedReact.EmailInput | |||||
label="Email" | |||||
name="email" | |||||
block | |||||
border | |||||
required | |||||
/> | |||||
</div> | |||||
<div className="col-span-2"> | |||||
<WebFreeformReact.MultilineTextInput | |||||
ref={messageRef} | |||||
label="Message" | |||||
name="message" | |||||
block | |||||
border | |||||
required | |||||
/> | |||||
</div> | |||||
<div className="col-span-2 text-right"> | |||||
<WebActionReact.ActionButton | |||||
type="submit" | |||||
variant="filled" | |||||
> | |||||
Send Message | |||||
</WebActionReact.ActionButton> | |||||
</div> | |||||
</div> | |||||
</fieldset> | |||||
</form> | |||||
)} | |||||
</Layouts.Basic.ContentContainer> | |||||
</div> | |||||
); | |||||
} |
@@ -0,0 +1,72 @@ | |||||
import * as React from 'react'; | |||||
import Link from 'next/link'; | |||||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||||
import * as WebNavigationReact from '@tesseract-design/web-navigation-react'; | |||||
import {ShowcaseItem, ShowcaseItemProps} from '@/components/molecules/ShowcaseItem'; | |||||
interface EnvisionSectionDatum extends ShowcaseItemProps { | |||||
id: string; | |||||
url: string; | |||||
} | |||||
export interface EnvisionSectionProps { | |||||
data: EnvisionSectionDatum[]; | |||||
} | |||||
export const EnvisionSection: React.FC<EnvisionSectionProps> = ({ | |||||
data, | |||||
}) => ( | |||||
<div className="py-12 sm:py-24 flex items-center min-h-full bg-negative relative"> | |||||
<Layouts.Basic.ContentContainer | |||||
span="wide" | |||||
> | |||||
<div className="pb-8"> | |||||
<h2 className="text-base leading-none m-0"> | |||||
<span className="text-7xl sm:text-8xl"> | |||||
I | |||||
{' '} | |||||
<Link | |||||
className="no-underline px-1 -mx-1" | |||||
href={{ | |||||
query: { | |||||
featured: 'all', | |||||
}, | |||||
}} | |||||
> | |||||
envision. | |||||
</Link> | |||||
</span> | |||||
</h2> | |||||
<div className="flex gap-2 font-headings lowercase text-3xl sm:text-4xl leading-none font-normal"> | |||||
<div><Link href={{ query: { featured: 'ideas' }}} className="p-1 -m-1 after:content-['.']">Ideas</Link></div> | |||||
<div><Link href={{ query: { featured: 'methods' }}} className="p-1 -m-1 after:content-['.']">Methods</Link></div> | |||||
<div><Link href={{ query: { featured: 'solutions' }}} className="p-1 -m-1 after:content-['.']">Solutions</Link></div> | |||||
</div> | |||||
</div> | |||||
<div className="flex flex-col xs:grid xs:grid-cols-2 gap-8"> | |||||
{data.map(({ id, url, ...datum }) => ( | |||||
<div key={id}> | |||||
<a href={url} className="block"> | |||||
<ShowcaseItem {...datum} /> | |||||
</a> | |||||
</div> | |||||
))} | |||||
</div> | |||||
<div className="flex justify-end mt-8"> | |||||
<div className="w-40"> | |||||
<WebNavigationReact.LinkButton | |||||
component={Link as unknown as 'a'} | |||||
href={{ | |||||
pathname: '/featured', | |||||
} as unknown as string} | |||||
menuItem | |||||
block | |||||
subtext="Featured" | |||||
> | |||||
Browse | |||||
</WebNavigationReact.LinkButton> | |||||
</div> | |||||
</div> | |||||
</Layouts.Basic.ContentContainer> | |||||
</div> | |||||
); |
@@ -0,0 +1,93 @@ | |||||
import * as React from 'react'; | |||||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||||
import * as WebNavigationReact from '@tesseract-design/web-navigation-react'; | |||||
interface IframeShowcaseProps extends Omit<React.HTMLProps<HTMLElementTagNameMap['iframe']>, 'children'> { | |||||
type: 'iframe'; | |||||
} | |||||
interface ImageShowcaseProps extends Omit<React.HTMLProps<HTMLElementTagNameMap['img']>, 'alt'> { | |||||
type: 'img'; | |||||
} | |||||
type ShowcaseProps = IframeShowcaseProps | ImageShowcaseProps; | |||||
interface FeaturedProjectLink { | |||||
href: string; | |||||
children: React.ReactNode; | |||||
primary?: boolean; | |||||
subtext?: React.ReactNode; | |||||
} | |||||
export interface FeaturedProjectSectionProps { | |||||
title: string; | |||||
description: string; | |||||
links?: FeaturedProjectLink[]; | |||||
showcase: ShowcaseProps; | |||||
} | |||||
export const FeaturedProjectSection: React.FC<FeaturedProjectSectionProps> = ({ | |||||
title, | |||||
description, | |||||
links = [], | |||||
showcase, | |||||
}) => { | |||||
const { type: showcaseType, ...showcaseProps } = showcase; | |||||
return ( | |||||
<div className="pt-24 sm:py-24 flex flex-col gap-8 sm:flex-row items-center sm:min-h-full bg-negative relative group"> | |||||
<div | |||||
className="absolute top-0 left-0 w-full h-full bg-shade opacity-25 group-even:opacity-0" | |||||
/> | |||||
<div className="sm:absolute top-0 left-0 w-full h-full"> | |||||
<Layouts.Basic.ContentContainer | |||||
span="wide" | |||||
className="relative sm:h-full" | |||||
> | |||||
<div className="sm:grid sm:grid-cols-5 sm:gap-32 sm:h-full"> | |||||
<div className="col-start-1 group-even:col-start-3 col-span-3 flex flex-col justify-center h-full gap-8"> | |||||
<h3 className="m-0 text-5xl font-light">{title}</h3> | |||||
<p className="m-0">{description}</p> | |||||
<div className="flex flex-wrap flex-row gap-4"> | |||||
{links.map(({ href, primary, subtext, children }) => ( | |||||
<div | |||||
key={href} | |||||
> | |||||
<WebNavigationReact.LinkButton | |||||
component="a" | |||||
href={href} | |||||
variant={primary ? 'filled' : 'outline'} | |||||
subtext={subtext} | |||||
target="_blank" | |||||
> | |||||
{children} | |||||
</WebNavigationReact.LinkButton> | |||||
</div> | |||||
))} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</Layouts.Basic.ContentContainer> | |||||
</div> | |||||
<div | |||||
className="border-0 m-0 sm:absolute sm:top-0 sm:right-0 group-even:sm:left-0 group-even:sm:right-0 w-full sm:w-2/5 h-96 sm:h-full" | |||||
> | |||||
{showcaseType === 'iframe' && ( | |||||
<iframe | |||||
{...showcaseProps as Omit<IframeShowcaseProps, 'type'>} | |||||
className={`border-0 m-0 w-full h-full ${showcaseProps.className ?? ''}`.trim()} | |||||
> | |||||
Cannot display embedded content. | |||||
</iframe> | |||||
)} | |||||
{showcaseType === 'img' && ( | |||||
<img | |||||
{...showcaseProps as Omit<ImageShowcaseProps, 'type'>} | |||||
className={`object-cover object-center border-0 m-0 w-full h-full block ${showcaseProps.className ?? ''}`.trim()} | |||||
alt={title} | |||||
/> | |||||
)} | |||||
</div> | |||||
</div> | |||||
); | |||||
}; |
@@ -0,0 +1,57 @@ | |||||
import * as React from 'react'; | |||||
import {Icon, IconProps} from '@/components/molecules/Icon'; | |||||
export interface FeedItemProps { | |||||
date: Date; | |||||
type: string; | |||||
description: string; | |||||
} | |||||
const formatFeedItemDate = (date: Date) => { | |||||
const mm = (date.getMonth() + 1).toString().padStart(2, '0'); | |||||
const dd = date.getDate().toString().padStart(2, '0'); | |||||
return `${mm}.${dd}`; | |||||
}; | |||||
const FEED_ITEM_TYPE_ICONS: Record<FeedItemProps['type'], IconProps['name']> = { | |||||
code: 'git-commit', | |||||
}; | |||||
export const FeedItem: React.FC<FeedItemProps> = ({ | |||||
date, | |||||
type, | |||||
description, | |||||
}) => ( | |||||
<div className="pb-[35%] sm:pb-[50%] relative"> | |||||
<div className="absolute top-0 left-0 w-full h-full"> | |||||
<dl className="h-full w-full relative p-2 rounded before:pointer-events-none before:rounded-inherit before:opacity-10 before:bg-current before:absolute before:top-0 before:left-0 before:w-full before:h-full grid"> | |||||
<div className="col-start-2 row-start-1 text-right text-xs font-semibold"> | |||||
<dt className="sr-only">Date</dt> | |||||
<dd> | |||||
<time dateTime={date.toISOString()}> | |||||
{formatFeedItemDate(date)} | |||||
</time> | |||||
</dd> | |||||
</div> | |||||
<div className="col-start-1 row-start-1"> | |||||
<dt className="sr-only">Type</dt> | |||||
<dd> | |||||
<Icon | |||||
name={FEED_ITEM_TYPE_ICONS[type]} | |||||
aria-label={type} | |||||
/> | |||||
</dd> | |||||
</div> | |||||
<div className="col-start-1 col-span-2 row-start-2 flex items-end text-xs relative"> | |||||
<dt className="sr-only">Description</dt> | |||||
<dd | |||||
className="absolute bottom-0 left-0 w-full line-clamp-3" | |||||
> | |||||
{description} | |||||
</dd> | |||||
</div> | |||||
</dl> | |||||
</div> | |||||
</div> | |||||
); |
@@ -0,0 +1,19 @@ | |||||
import * as React from 'react'; | |||||
export interface FeedItemHeadingProps { | |||||
children?: React.ReactNode; | |||||
} | |||||
export const FeedItemHeading: React.FC<FeedItemHeadingProps> = ({ | |||||
children, | |||||
}) => ( | |||||
<span className="block pb-[35%] sm:pb-[50%] relative"> | |||||
<span className="absolute top-0 left-0 w-full h-full block"> | |||||
<span | |||||
className="block text-2xl leading-none h-full w-full relative px-2 py-2.5 rounded before:pointer-events-none before:rounded-inherit before:border-2 before:absolute before:top-0 before:left-0 before:w-full before:h-full" | |||||
> | |||||
{children} | |||||
</span> | |||||
</span> | |||||
</span> | |||||
); |
@@ -0,0 +1,151 @@ | |||||
import * as React from 'react'; | |||||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||||
import {Brand} from '@/components/molecules/Brand'; | |||||
export const Footer = () => ( | |||||
<footer className="py-12 sm:py-24 flex items-center relative"> | |||||
<div | |||||
className="absolute top-0 left-0 w-full h-full bg-shade opacity-75" | |||||
/> | |||||
<Layouts.Basic.ContentContainer | |||||
span="wide" | |||||
className="flex flex-col gap-12 relative" | |||||
> | |||||
<div | |||||
className="flex flex-col sm:flex-row gap-8 sm:gap-12" | |||||
> | |||||
<h2 className="m-0 flex-auto shrink-0"> | |||||
See you around. | |||||
</h2> | |||||
<dl | |||||
className="flex flex-col xs:grid gap-8 xs:gap-12 xs:grid-cols-3" | |||||
> | |||||
<div className="flex flex-col gap-4"> | |||||
<dt className="text-xs uppercase font-bold"> | |||||
</dt> | |||||
<dd className="text-xs font-mono leading-normal"> | |||||
<a | |||||
href="https://linkedin.com/in/allancrisostomo" | |||||
rel="noreferrer noopener" | |||||
target="_blank" | |||||
className="no-underline before:content-['/in/'] p-1 -m-1" | |||||
> | |||||
allancrisostomo | |||||
</a> | |||||
</dd> | |||||
</div> | |||||
<div className="flex flex-col gap-4"> | |||||
<dt className="text-xs uppercase font-bold"> | |||||
Modal Code | |||||
</dt> | |||||
<dd className="text-xs font-mono leading-normal"> | |||||
<a | |||||
href="https://code.modal.sh/TheoryOfNekomata" | |||||
rel="noreferrer noopener" | |||||
target="_blank" | |||||
className="no-underline before:content-['@'] p-1 -m-1" | |||||
> | |||||
TheoryOfNekomata | |||||
</a> | |||||
</dd> | |||||
</div> | |||||
<div className="flex flex-col gap-4"> | |||||
<dt className="text-xs uppercase font-bold"> | |||||
GitHub | |||||
</dt> | |||||
<dd className="text-xs font-mono leading-normal"> | |||||
<a | |||||
href="https://github.com/TheoryOfNekomata" | |||||
rel="noreferrer noopener" | |||||
target="_blank" | |||||
className="no-underline before:content-['@'] p-1 -m-1" | |||||
> | |||||
TheoryOfNekomata | |||||
</a> | |||||
</dd> | |||||
</div> | |||||
<div className="flex flex-col gap-4"> | |||||
<dt className="text-xs uppercase font-bold"> | |||||
Soundcloud | |||||
</dt> | |||||
<dd className="text-xs font-mono leading-normal"> | |||||
<a | |||||
href="https://soundcloud.com/TheoryOfNekomata" | |||||
rel="noreferrer noopener" | |||||
target="_blank" | |||||
className="no-underline before:content-['@'] p-1 -m-1" | |||||
> | |||||
TheoryOfNekomata | |||||
</a> | |||||
</dd> | |||||
</div> | |||||
<div className="flex flex-col gap-4"> | |||||
<dt className="text-xs uppercase font-bold"> | |||||
YouTube | |||||
</dt> | |||||
<dd className="text-xs font-mono leading-normal"> | |||||
<a | |||||
href="https://youtube.com/@TheoryOfNekomata" | |||||
rel="noreferrer noopener" | |||||
target="_blank" | |||||
className="no-underline before:content-['@'] p-1 -m-1" | |||||
> | |||||
TheoryOfNekomata | |||||
</a> | |||||
</dd> | |||||
</div> | |||||
<div className="flex flex-col gap-4"> | |||||
<dt className="text-xs uppercase font-bold"> | |||||
Pixiv | |||||
</dt> | |||||
<dd className="text-xs font-mono leading-normal"> | |||||
<a | |||||
href="https://pixiv.me/theoryofnekomata" | |||||
rel="noreferrer noopener" | |||||
target="_blank" | |||||
className="no-underline before:content-['@'] p-1 -m-1" | |||||
> | |||||
theoryofnekomata | |||||
</a> | |||||
</dd> | |||||
</div> | |||||
<div className="flex flex-col gap-4"> | |||||
<dt className="text-xs uppercase font-bold"> | |||||
X (formerly Twitter) | |||||
</dt> | |||||
<dd className="text-xs font-mono leading-normal"> | |||||
<a | |||||
href="https://twitter.com/theoryofnekomata" | |||||
rel="noreferrer noopener" | |||||
target="_blank" | |||||
className="no-underline before:content-['@'] p-1 -m-1" | |||||
> | |||||
theoryofnekomata | |||||
</a> | |||||
</dd> | |||||
<dd className="text-xs font-mono leading-normal"> | |||||
<a | |||||
href="https://twitter.com/clearfix.5essnce" | |||||
rel="noreferrer noopener" | |||||
target="_blank" | |||||
className="no-underline before:content-['@'] p-1 -m-1" | |||||
> | |||||
clearfix.5essnce | |||||
</a> | |||||
</dd> | |||||
</div> | |||||
</dl> | |||||
</div> | |||||
<div className="text-center"> | |||||
© | |||||
{' '} | |||||
<span className="text-2xl mx-3"> | |||||
<Brand/> | |||||
</span> | |||||
{' '} | |||||
{new Date().getFullYear()}. | |||||
</div> | |||||
</Layouts.Basic.ContentContainer> | |||||
</footer> | |||||
) |
@@ -0,0 +1,42 @@ | |||||
import * as React from 'react'; | |||||
const ICONS = { | |||||
'git-commit': ( | |||||
<> | |||||
<circle cx="12" cy="12" r="4" /> | |||||
<line x1="1.05" y1="12" x2="7" y2="12" /> | |||||
<line x1="17.01" y1="12" x2="22.96" y2="12" /> | |||||
</> | |||||
), | |||||
'scroll-down': ( | |||||
<> | |||||
<polyline points="7 13 12 18 17 13" /> | |||||
<polyline points="7 6 12 11 17 6" /> | |||||
</> | |||||
) | |||||
}; | |||||
export const IconDerivedElementComponent = 'svg' as const; | |||||
export type IconDerivedElement = SVGElementTagNameMap[typeof IconDerivedElementComponent]; | |||||
type IconName = keyof typeof ICONS; | |||||
export interface IconProps extends React.SVGProps<IconDerivedElement> { | |||||
name: IconName; | |||||
} | |||||
export const Icon = React.forwardRef<IconDerivedElement, IconProps>(({ | |||||
name, | |||||
className, | |||||
...etcProps | |||||
}, forwardedRef) => ( | |||||
<IconDerivedElementComponent | |||||
{...etcProps} | |||||
ref={forwardedRef} | |||||
viewBox="0 0 24 24" | |||||
className={`w-6 h-6 linejoin-round linecap-round stroke-2 stroke-current fill-none ${className ?? ''}`.trim()} | |||||
> | |||||
{ICONS[name]} | |||||
</IconDerivedElementComponent> | |||||
)); |
@@ -0,0 +1,117 @@ | |||||
import * as React from 'react'; | |||||
import Link from 'next/link'; | |||||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||||
import {Brand} from '@/components/molecules/Brand'; | |||||
import {Pillar} from '@/components/molecules/Pillar'; | |||||
import {BackgroundGrid} from '@/components/molecules/BackgroundGrid'; | |||||
import {Icon} from '@/components/molecules/Icon'; | |||||
export interface MainLandingSectionProps { | |||||
backgroundImages: string[]; | |||||
} | |||||
export const MainLandingSection: React.FC<MainLandingSectionProps> = ({ | |||||
backgroundImages | |||||
}) => { | |||||
const scrollDown: React.MouseEventHandler<HTMLAnchorElement> = React.useCallback((event) => { | |||||
event.preventDefault(); | |||||
const target = window.document.querySelector(event.currentTarget.getAttribute('href')!); | |||||
if (!target) { | |||||
return; | |||||
} | |||||
target.scrollIntoView({ | |||||
behavior: 'smooth', | |||||
}); | |||||
}, []); | |||||
return ( | |||||
<div | |||||
className="sm:py-24 flex items-center min-h-full relative overflow-hidden" | |||||
> | |||||
<div | |||||
className="fixed top-0 left-0 w-full h-full" | |||||
> | |||||
<div className="w-[125%] h-[125%] absolute -top-[12.5%] -left-[12.5%] opacity-25"> | |||||
<BackgroundGrid images={backgroundImages} /> | |||||
</div> | |||||
</div> | |||||
<div | |||||
className="absolute top-0 left-0 w-full h-full bg-shade opacity-25" | |||||
/> | |||||
<Layouts.Basic.ContentContainer | |||||
span="wide" | |||||
className="relative py-24" | |||||
> | |||||
<div className="flex flex-col sm:flex-row sm:items-center gap-8 sm:gap-16"> | |||||
<h1 className="text-center m-0 before:text-7xl sm:before:text-8xl before:block before:content-['I_am.'] text-base leading-none"> | |||||
<span className="text-4xl"> | |||||
<Brand/> | |||||
</span> | |||||
</h1> | |||||
<div className="w-full sm:w-auto flex-auto flex flex-row sm:flex-col gap-2 h-64 sm:h-64"> | |||||
<div className="w-0 sm:w-auto flex-auto"> | |||||
<Link | |||||
href="/credentials" | |||||
className="block w-full h-full" | |||||
> | |||||
<Pillar> | |||||
Credentials | |||||
</Pillar> | |||||
</Link> | |||||
</div> | |||||
<div className="w-0 sm:w-auto flex-auto"> | |||||
<Link | |||||
href="/portfolio" | |||||
className="block w-full h-full" | |||||
> | |||||
<Pillar> | |||||
Portfolio | |||||
</Pillar> | |||||
</Link> | |||||
</div> | |||||
<div className="w-0 sm:w-auto flex-auto"> | |||||
<Link | |||||
href="/blog" | |||||
className="block w-full h-full" | |||||
> | |||||
<Pillar> | |||||
Blog | |||||
</Pillar> | |||||
</Link> | |||||
</div> | |||||
<div className="w-0 sm:w-auto flex-auto"> | |||||
<Link | |||||
href="/services-values" | |||||
className="block w-full h-full" | |||||
> | |||||
<Pillar> | |||||
Services & Values | |||||
</Pillar> | |||||
</Link> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</Layouts.Basic.ContentContainer> | |||||
<div className="absolute bottom-0 left-0 w-full h-24 flex justify-center items-center"> | |||||
<div className="w-12 h-12"> | |||||
<a | |||||
href="#start" | |||||
className="relative rounded-full overflow-hidden w-12 h-12 block" | |||||
onClick={scrollDown} | |||||
> | |||||
<span | |||||
className="absolute top-0 left-0 w-full h-full bg-current opacity-75" | |||||
/> | |||||
<span className="text-negative rounded-inherit absolute top-0 left-0 w-full h-full flex items-center justify-center"> | |||||
<Icon | |||||
name="scroll-down" | |||||
/> | |||||
</span> | |||||
</a> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
); | |||||
} |
@@ -0,0 +1,115 @@ | |||||
import * as React from 'react'; | |||||
import Link from 'next/link'; | |||||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||||
import * as WebNavigationReact from '@tesseract-design/web-navigation-react'; | |||||
import {FeedItem, FeedItemProps} from '@/components/molecules/FeedItem'; | |||||
import {FeedItemHeading} from '@/components/molecules/FeedItemHeading'; | |||||
interface MakeSectionDatum extends FeedItemProps { | |||||
id: string; | |||||
url: string; | |||||
} | |||||
export interface MakeSectionProps { | |||||
data: MakeSectionDatum[]; | |||||
} | |||||
export const MakeSection: React.FC<MakeSectionProps> = ({ | |||||
data, | |||||
}) => { | |||||
const groupedData = data.reduce( | |||||
(grouped, datum) => ({ | |||||
...grouped, | |||||
[datum.date.getFullYear()]: [ | |||||
...(grouped[datum.date.getFullYear()] ?? []), | |||||
datum, | |||||
], | |||||
}), | |||||
{} as Record<string, MakeSectionDatum[]>, | |||||
); | |||||
return ( | |||||
<div className="py-12 sm:py-24 flex items-center min-h-full relative bg-negative"> | |||||
<div | |||||
className="absolute top-0 left-0 w-full h-full bg-shade opacity-25" | |||||
/> | |||||
<Layouts.Basic.ContentContainer | |||||
span="wide" | |||||
className="relative" | |||||
> | |||||
<div className="pb-8"> | |||||
<h2 className="text-base leading-none m-0"> | |||||
<span className="text-7xl sm:text-8xl"> | |||||
I | |||||
{' '} | |||||
<Link | |||||
className="no-underline px-1 -mx-1" | |||||
href={{ | |||||
query: { | |||||
feed: 'all', | |||||
}, | |||||
}} | |||||
> | |||||
make. | |||||
</Link> | |||||
</span> | |||||
</h2> | |||||
<div className="flex gap-2 font-headings lowercase text-3xl sm:text-4xl leading-none font-normal"> | |||||
<div><Link href={{ query: { feed: 'software' }}} className="p-1 -m-1 after:content-['.']">Software</Link></div> | |||||
<div><Link href={{ query: { feed: 'music' }}} className="p-1 -m-1 after:content-['.']">Music</Link></div> | |||||
<div><Link href={{ query: { feed: 'art' }}} className="p-1 -m-1 after:content-['.']">Art</Link></div> | |||||
</div> | |||||
</div> | |||||
<div | |||||
className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 gap-2" | |||||
> | |||||
{Object.entries(groupedData).map(([year, data]) => ( | |||||
<section | |||||
key={year} | |||||
className="contents" | |||||
> | |||||
<h3 | |||||
className="text-base m-0" | |||||
> | |||||
<a | |||||
href="#" | |||||
className="block" | |||||
> | |||||
<FeedItemHeading> | |||||
{year} | |||||
</FeedItemHeading> | |||||
</a> | |||||
</h3> | |||||
<div className="contents"> | |||||
{data.map(({ id, ...datum }) => ( | |||||
<div | |||||
key={id} | |||||
> | |||||
<a href="#" className="block"> | |||||
<FeedItem {...datum} /> | |||||
</a> | |||||
</div> | |||||
))} | |||||
</div> | |||||
</section> | |||||
))} | |||||
</div> | |||||
<div className="flex justify-end mt-8"> | |||||
<div className="w-40"> | |||||
<WebNavigationReact.LinkButton | |||||
component={Link as unknown as 'a'} | |||||
href={{ | |||||
pathname: '/feed', | |||||
} as unknown as string} | |||||
menuItem | |||||
block | |||||
subtext="Feed" | |||||
> | |||||
Browse | |||||
</WebNavigationReact.LinkButton> | |||||
</div> | |||||
</div> | |||||
</Layouts.Basic.ContentContainer> | |||||
</div> | |||||
); | |||||
} |
@@ -0,0 +1,16 @@ | |||||
import * as React from 'react'; | |||||
export interface PillarProps { | |||||
children?: React.ReactNode; | |||||
} | |||||
export const Pillar: React.FC<PillarProps> = ({ | |||||
children, | |||||
}) => ( | |||||
<span className="block h-full w-full relative rounded overflow-hidden"> | |||||
<span className="absolute top-0 left-0 w-full h-full bg-current opacity-75" /> | |||||
<span className="text-negative absolute bottom-0 right-0 sm:right-auto sm:left-0 text-sm uppercase font-bold whitespace-nowrap p-2 origin-bottom-left -rotate-90 translate-x-full sm:translate-x-0 sm:rotate-0"> | |||||
{children} | |||||
</span> | |||||
</span> | |||||
); |
@@ -0,0 +1,34 @@ | |||||
import * as React from 'react'; | |||||
export interface ShowcaseItemProps { | |||||
imageUrl?: string; | |||||
title: string; | |||||
description: string; | |||||
} | |||||
export const ShowcaseItem: React.FC<ShowcaseItemProps> = ({ | |||||
imageUrl, | |||||
title, | |||||
description, | |||||
}) => ( | |||||
<div className="relative rounded overflow-hidden"> | |||||
<div className="bg-current absolute top-0 left-0 w-full h-full opacity-10" /> | |||||
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-4 relative"> | |||||
<div className="shrink-0 w-full sm:max-w-[8rem]"> | |||||
<span className="w-full pb-[75%] relative block"> | |||||
<span className="absolute block w-full h-full"> | |||||
<img | |||||
src={imageUrl ?? 'http://placehold.it/1'} | |||||
className="w-full h-full" | |||||
alt={title} | |||||
/> | |||||
</span> | |||||
</span> | |||||
</div> | |||||
<div className="flex-auto min-w-0 flex flex-col gap-2 p-4 sm:py-2"> | |||||
<h3 className="m-0 whitespace-nowrap text-ellipsis overflow-hidden w-full text-2xl">{title}</h3> | |||||
<p className="m-0 line-clamp-2 text-xs">{description}</p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
); |
@@ -0,0 +1,47 @@ | |||||
import * as React from 'react'; | |||||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||||
import {MainLandingSection, MainLandingSectionProps} from '@/components/molecules/MainLandingSection'; | |||||
import {MakeSection, MakeSectionProps} from '@/components/molecules/MakeSection'; | |||||
import {EnvisionSection, EnvisionSectionProps} from '@/components/molecules/EnvisionSection'; | |||||
import {Footer} from '@/components/molecules/Footer'; | |||||
import {FeaturedProjectSection, FeaturedProjectSectionProps} from '@/components/molecules/FeaturedProjectSection'; | |||||
import {ContactCtaBanner} from '@/components/molecules/ContactCtaBanner'; | |||||
export interface IndexLayoutProps { | |||||
backgroundImages: MainLandingSectionProps['backgroundImages']; | |||||
makeSectionData: MakeSectionProps['data']; | |||||
envisionSectionData: EnvisionSectionProps['data']; | |||||
featuredProjectData?: FeaturedProjectSectionProps[]; | |||||
onSubmit?: React.FormEventHandler<HTMLFormElement>; | |||||
} | |||||
export const IndexLayout: React.FC<IndexLayoutProps> = ({ | |||||
backgroundImages, | |||||
makeSectionData, | |||||
envisionSectionData, | |||||
featuredProjectData = [], | |||||
onSubmit, | |||||
}) => ( | |||||
<Layouts.Basic.Root className="contents"> | |||||
<MainLandingSection | |||||
backgroundImages={backgroundImages} | |||||
/> | |||||
<div id="start" /> | |||||
<MakeSection | |||||
data={makeSectionData} | |||||
/> | |||||
<EnvisionSection | |||||
data={envisionSectionData} | |||||
/> | |||||
{featuredProjectData.map((featuredProjectProps) => ( | |||||
<FeaturedProjectSection | |||||
key={featuredProjectProps.title} | |||||
{...featuredProjectProps} | |||||
/> | |||||
))} | |||||
<ContactCtaBanner | |||||
onSubmit={onSubmit} | |||||
/> | |||||
<Footer/> | |||||
</Layouts.Basic.Root> | |||||
); |
@@ -0,0 +1,51 @@ | |||||
import * as React from 'react'; | |||||
const DEFAULT_COLOR_HUE_DEGREES = 320 as const; | |||||
const DEFAULT_ANIMATION_DURATION = 60000 as const; | |||||
const DEFAULT_UPDATE_INTERVAL_MS = 50 as const; | |||||
const DEFAULT_COLOR_SATURATION_PERCENTAGE = 35 as const; | |||||
const DEFAULT_COLOR_LIGHTNESS_PERCENTAGE = 66 as const; | |||||
const MAX_HUE = 360 as const; | |||||
export interface UseHuePulsateOptions { | |||||
animationDuration?: number; | |||||
initialColorHueDegrees?: number; | |||||
colorSaturationPercentage?: number; | |||||
colorLightnessPercentage?: number; | |||||
updateIntervalMs?: number; | |||||
propertyName: string; | |||||
} | |||||
export const useHuePulsate = (options : UseHuePulsateOptions) => { | |||||
React.useEffect(() => { | |||||
const { | |||||
animationDuration = DEFAULT_ANIMATION_DURATION, | |||||
initialColorHueDegrees = DEFAULT_COLOR_HUE_DEGREES, | |||||
updateIntervalMs = DEFAULT_UPDATE_INTERVAL_MS, | |||||
colorSaturationPercentage = DEFAULT_COLOR_SATURATION_PERCENTAGE, | |||||
colorLightnessPercentage = DEFAULT_COLOR_LIGHTNESS_PERCENTAGE, | |||||
propertyName, | |||||
} = options; | |||||
const start = Date.now(); | |||||
const intervalHandle = window.setInterval(() => { | |||||
//window.document.documentElement.style.setProperty('--scroll-y', `${window.scrollY}px`); | |||||
const elapsed = Date.now() - start; | |||||
const progress = elapsed / animationDuration; | |||||
const colorPrimaryHue = (initialColorHueDegrees + (progress * MAX_HUE)) % MAX_HUE; | |||||
window.document.documentElement.style.setProperty( | |||||
propertyName, | |||||
`${colorPrimaryHue} ${colorSaturationPercentage}% ${colorLightnessPercentage}%` | |||||
); | |||||
}, updateIntervalMs); | |||||
return () => { | |||||
window.clearInterval(intervalHandle); | |||||
}; | |||||
}, []); | |||||
}; |
@@ -0,0 +1,6 @@ | |||||
import '@/styles/globals.css' | |||||
import type { AppProps } from 'next/app' | |||||
export default function App({ Component, pageProps }: AppProps) { | |||||
return <Component {...pageProps} /> | |||||
} |
@@ -0,0 +1,24 @@ | |||||
import { Html, Head, Main, NextScript } from 'next/document' | |||||
import theme from '@/styles/theme' | |||||
export default function Document() { | |||||
return ( | |||||
<Html lang="en-PH" className="w-full h-full bg-negative text-positive tracking-normal font-semi-expanded"> | |||||
<Head> | |||||
<style | |||||
dangerouslySetInnerHTML={{ | |||||
__html: ` | |||||
:root { | |||||
${Object.entries(theme).map(([name, value]) => `--${name}: ${value};`).join('\n')} | |||||
} | |||||
`, | |||||
}} | |||||
/> | |||||
</Head> | |||||
<body className="w-full h-full"> | |||||
<Main /> | |||||
<NextScript /> | |||||
</body> | |||||
</Html> | |||||
) | |||||
} |
@@ -0,0 +1,13 @@ | |||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction | |||||
import type { NextApiRequest, NextApiResponse } from 'next' | |||||
type Data = { | |||||
name: string | |||||
} | |||||
export default function handler( | |||||
req: NextApiRequest, | |||||
res: NextApiResponse<Data> | |||||
) { | |||||
res.status(200).json({ name: 'John Doe' }) | |||||
} |
@@ -0,0 +1,134 @@ | |||||
import type { NextPage } from 'next'; | |||||
import * as React from 'react'; | |||||
import {useHuePulsate} from '@/hooks/effects'; | |||||
import {IndexLayout} from '@/components/organisms/IndexLayout'; | |||||
import {getFormValues} from '@theoryofnekomata/formxtra'; | |||||
const IMAGE_CHOICES = [ | |||||
'/images/3cd237361eada7fd30eb96d42d55ec00.jpg', | |||||
'/images/5ace16248237a96f6dbbcc16a3c385fe.jpg', | |||||
'/images/6af0fdc5eafb86f8b540e3d322c3a007.jpg', | |||||
'/images/8f5e3d92d2da0a760b312a9387219447.jpg', | |||||
'/images/162cab44da8420c50900d8f11c6da6fd.jpg', | |||||
'/images/2626c3485668a19730c64a7ee672a214.jpg', | |||||
'/images/9802f260887e9044312fb7d88547fb09.jpg', | |||||
'/images/a3a56f54a11c76f46f4392b748f3026a.png', | |||||
'/images/a96d02d8cc43cc3b9f1e77c43dfa5644.png', | |||||
'/images/d69ab7f4e5747b5b8f297e4ebe9770f5.png', | |||||
'/images/d1454662da2a4a8e77cd4c98eda6d662.png', | |||||
'/images/e4753cf6a55db6b7ab5037ed0fd4a3fe.jpg', | |||||
'/images/fe3050a31d3fb86accf26cc4bebec102.png', | |||||
]; | |||||
const makeSectionData = new Array(20).fill(null).map((_, index) => ({ | |||||
id: (index + 1).toString(), | |||||
url: 'http://www.example.com', | |||||
date: new Date('2023-07-03'), | |||||
type: 'code', | |||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae diam eget nunc aliquam vestibulum. Sed vitae diam eget nunc aliquam vestibulum.', | |||||
})); | |||||
const envisionSectionData = new Array(6).fill(null).map((_, index) => ({ | |||||
id: (index + 1).toString(), | |||||
url: 'http://www.example.com', | |||||
imageUrl: 'http://placehold.it/1', | |||||
title: 'Lorem ipsum dolor sit amet', | |||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae diam eget nunc aliquam vestibulum. Sed vitae diam eget nunc aliquam vestibulum.', | |||||
})); | |||||
const featuredProjectData = [ | |||||
{ | |||||
title: 'formxtra', | |||||
description: "Extract and set form values through the DOM—no frameworks required!", | |||||
links:[ | |||||
{ | |||||
href: 'https://codepen.io/theoryofnekomata/pen/xxajmvJ', | |||||
children: 'View Demo', | |||||
}, | |||||
{ | |||||
href: 'https://code.modal.sh/TheoryOfNekomata/formxtra', | |||||
children: 'Explore Code', | |||||
primary: true, | |||||
}, | |||||
], | |||||
showcase: { | |||||
type: 'iframe' as const, | |||||
allowFullScreen: true, | |||||
src: 'https://codepen.io/theoryofnekomata/embed/xxajmvJ?default-tab=result&theme-id=dark', | |||||
sandbox: 'allow-forms allow-modals allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation-by-user-activation allow-downloads allow-presentation', | |||||
allow: 'accelerometer; camera; encrypted-media; display-capture; geolocation; gyroscope; microphone; midi; clipboard-read; clipboard-write; web-share', | |||||
}, | |||||
}, | |||||
{ | |||||
title: 'tesseract', | |||||
description: 'Functional, accessible, aesthetic design system.', | |||||
links:[ | |||||
{ | |||||
href: 'https://code.modal.sh/tesseract-design/tesseract', | |||||
children: 'Explore Code', | |||||
primary: true, | |||||
}, | |||||
], | |||||
showcase: { | |||||
type: 'img' as const, | |||||
src: '/images/162cab44da8420c50900d8f11c6da6fd.jpg', | |||||
}, | |||||
}, | |||||
{ | |||||
title: 'numerica', | |||||
description: 'Gets the name of a number, even if it\'s stupidly big.', | |||||
links:[ | |||||
{ | |||||
href: 'https://code.modal.sh/modal-soft/numerica', | |||||
children: 'Explore Code', | |||||
primary: true, | |||||
}, | |||||
], | |||||
showcase: { | |||||
type: 'img' as const, | |||||
src: '/images/d69ab7f4e5747b5b8f297e4ebe9770f5.png', | |||||
}, | |||||
}, | |||||
{ | |||||
title: 'webvideo-transcript-summarizer', | |||||
description: 'Get transcript summaries of Web videos. Powered by OpenAI.', | |||||
links:[ | |||||
{ | |||||
href: 'https://code.modal.sh/modal-soft/webvideo-transcript-summary', | |||||
children: 'Explore Code', | |||||
primary: true, | |||||
}, | |||||
], | |||||
showcase: { | |||||
type: 'img' as const, | |||||
src: '/images/3cd237361eada7fd30eb96d42d55ec00.jpg', | |||||
}, | |||||
}, | |||||
]; | |||||
const IndexPage: NextPage = () => { | |||||
useHuePulsate({ | |||||
propertyName: '--color-primary', | |||||
initialColorHueDegrees: 320, | |||||
colorSaturationPercentage: 35, | |||||
colorLightnessPercentage: 66, | |||||
}); | |||||
const processContactForm: React.FormEventHandler<HTMLFormElement> = (e) => { | |||||
e.preventDefault(); | |||||
const values = getFormValues(e.currentTarget); | |||||
console.log(values); | |||||
}; | |||||
return ( | |||||
<IndexLayout | |||||
backgroundImages={IMAGE_CHOICES} | |||||
makeSectionData={makeSectionData} | |||||
envisionSectionData={envisionSectionData} | |||||
featuredProjectData={featuredProjectData} | |||||
onSubmit={processContactForm} | |||||
/> | |||||
); | |||||
}; | |||||
export default IndexPage; |
@@ -0,0 +1,197 @@ | |||||
@tailwind base; | |||||
@tailwind components; | |||||
@tailwind utilities; | |||||
input, | |||||
textarea, | |||||
button, | |||||
select, | |||||
a { | |||||
-webkit-tap-highlight-color: transparent; | |||||
} | |||||
#__next { | |||||
display: contents; | |||||
} | |||||
/* | |||||
@property --color-primary-hue { | |||||
syntax: '<number>'; | |||||
initial-value: 0; | |||||
inherits: false; | |||||
} | |||||
@keyframes rgb-pulsate { | |||||
0% { | |||||
--color-primary-hue: 320; | |||||
} | |||||
100% { | |||||
--color-primary-hue: 680; | |||||
} | |||||
} | |||||
:root { | |||||
animation: rgb-pulsate 60000ms infinite; | |||||
--color-primary: var(--color-primary-hue) 35% 66%; | |||||
} | |||||
*/ | |||||
/* TODO add to tesseract plugin */ | |||||
@layer base { | |||||
:root { | |||||
--color-sidebar: 29 29 29; | |||||
--color-topbar: 19 19 19; | |||||
--color-sidebar-menu: 24 24 24; | |||||
} | |||||
h1 { | |||||
@apply font-headings lowercase text-5xl font-thin leading-none my-8; | |||||
} | |||||
h2 { | |||||
@apply font-headings lowercase text-4xl font-light leading-none my-8; | |||||
} | |||||
h3 { | |||||
@apply font-headings lowercase text-3xl leading-none my-8; | |||||
} | |||||
h4 { | |||||
@apply font-headings lowercase text-2xl leading-none my-8; | |||||
} | |||||
h5 { | |||||
@apply font-headings lowercase text-xl leading-none my-8; | |||||
} | |||||
h6 { | |||||
@apply font-headings lowercase text-lg leading-none my-8; | |||||
} | |||||
p { | |||||
@apply my-8; | |||||
} | |||||
small { | |||||
font-size: 0.75em; | |||||
} | |||||
a[href]:not([class]) { | |||||
@apply p-1 -m-1 underline; | |||||
} | |||||
a[href] { | |||||
@apply text-primary ring-secondary/50; | |||||
} | |||||
a[href]:not([class~="rounded-full"]) { | |||||
@apply rounded | |||||
} | |||||
a[href]:focus { | |||||
@apply ring-4 text-secondary; | |||||
outline: 0; | |||||
} | |||||
button:hover { | |||||
@apply ring-4 text-secondary; | |||||
} | |||||
a[href]:hover { | |||||
@apply ring-4 text-secondary; | |||||
} | |||||
a[href]:active { | |||||
@apply ring-tertiary/50 text-tertiary; | |||||
} | |||||
a[href].focus\:text-negative:focus { | |||||
color: rgb(var(--color-negative)); | |||||
} | |||||
a[href].active\:text-negative:active { | |||||
color: rgb(var(--color-negative)); | |||||
} | |||||
/*pre {*/ | |||||
/* overflow: auto;*/ | |||||
/* margin: 0 -1rem;*/ | |||||
/* padding: 0 1rem;*/ | |||||
/* line-height: 1.2;*/ | |||||
/* box-sizing: border-box;*/ | |||||
/*}*/ | |||||
@media only print { | |||||
pre, pre * { | |||||
color: inherit !important; | |||||
} | |||||
code, code * { | |||||
color: inherit !important; | |||||
} | |||||
img { | |||||
filter: grayscale(100%); | |||||
} | |||||
:root { | |||||
--color-accent: black !important; | |||||
--color-active: black !important; | |||||
} | |||||
} | |||||
} | |||||
:root .rti--container { | |||||
--rti-bg: transparent; | |||||
--rti-border: transparent; | |||||
--rti-main: transparent; | |||||
--rti-radius: 0; | |||||
--rti-s: 0.5rem; | |||||
--rti-tag: transparent; | |||||
--rti-tag-remove: transparent; | |||||
--rti-tag-padding: 0 0; | |||||
} | |||||
.highlight .token.number { color: rgb(var(--color-code-number)); } | |||||
.highlight .token.keyword { color: rgb(var(--color-code-keyword)); } | |||||
.highlight .token.tag { color: rgb(var(--color-code-keyword)); } | |||||
.highlight .token.type { color: rgb(var(--color-code-type)); } | |||||
.highlight .token.instance-attribute { color: rgb(var(--color-code-instance-attribute)); } | |||||
.highlight .token.maybe-class-name { color: rgb(var(--color-code-function)); font-style: italic; } | |||||
.highlight .token.function { color: rgb(var(--color-code-function)); font-style: italic; } | |||||
.highlight .token.parameter { color: rgb(var(--color-code-parameter)); } | |||||
.highlight .token.property { color: rgb(var(--color-code-property)); } | |||||
.highlight .token.attr-name { color: rgb(var(--color-code-property)); font-style: italic; } | |||||
.highlight .token.string { color: rgb(var(--color-code-string)); } | |||||
.highlight .token.attr-value { color: rgb(var(--color-code-string)); } | |||||
.highlight .token.attr-value .attr-equals { color: rgb(var(--color-positive)); } | |||||
.highlight .token.variable { color: rgb(var(--color-code-variable)); } | |||||
.highlight .token.regexp { color: rgb(var(--color-code-regexp)); } | |||||
.highlight .token.url { color: rgb(var(--color-code-url)); } | |||||
.highlight .token.global { color: rgb(var(--color-code-global)); } | |||||
.highlight .token.comment { opacity: 0.5; } | |||||
.highlight .x00 { color: rgb(var(--color-code-keyword)); } | |||||
.highlight .x10 { color: rgb(var(--color-code-global)); } | |||||
.highlight .x20 { color: rgb(var(--color-code-string)); } | |||||
.highlight .x30 { color: rgb(var(--color-code-number)); } | |||||
.highlight .x40 { color: rgb(var(--color-code-url)); } | |||||
.highlight .x50 { color: rgb(var(--color-code-type)); } | |||||
.highlight .x60 { color: rgb(var(--color-code-parameter)); } | |||||
.highlight .x70 { color: rgb(var(--color-code-property)); } | |||||
.highlight .x80 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-keyword)); } | |||||
.highlight .x90 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-global)); } | |||||
.highlight .xa0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-string)); } | |||||
.highlight .xb0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-number)); } | |||||
.highlight .xc0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-url)); } | |||||
.highlight .xd0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-type)); } | |||||
.highlight .xe0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-parameter)); } | |||||
.highlight .xf0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-property)); } | |||||
.focus\:outline-0:-moz-focusring { | |||||
outline: 0; | |||||
} |
@@ -0,0 +1,25 @@ | |||||
const theme = { | |||||
"color-shade": "0 0 0", | |||||
"color-negative": "34 34 34", | |||||
"color-positive": "238 238 238", | |||||
"color-primary": "320 35% 66%", | |||||
"color-secondary": "215 95 75", | |||||
"color-tertiary": "255 153 0", | |||||
"color-code-number": "116 249 94", | |||||
"color-code-keyword": "255 67 137", | |||||
"color-code-type": "80 151 210", | |||||
"color-code-instance-attribute": "118 167 210", | |||||
"color-code-function": "103 194 82", | |||||
"color-code-parameter": "145 94 194", | |||||
"color-code-property": "255 161 201", | |||||
"color-code-string": "238 211 113", | |||||
"color-code-variable": "139 194 117", | |||||
"color-code-regexp": "116 167 43", | |||||
"color-code-url": "0 153 204", | |||||
"color-code-global": "194 128 80", | |||||
"font-sans": '"Encode Sans"', | |||||
"font-headings": 'Glory, var(--font-sans)', | |||||
"font-mono": 'MonoLisa, mononoki', | |||||
} as const; | |||||
export default theme; |
@@ -0,0 +1,132 @@ | |||||
import defaultTheme from 'tailwindcss/defaultTheme'; | |||||
import plugin from 'tailwindcss/plugin'; | |||||
import type { Config } from 'tailwindcss'; | |||||
import { tailwind } from '@tesseract-design/viewfinder-base'; | |||||
const tesseractPlugin = plugin( | |||||
({ addUtilities }) => { | |||||
addUtilities({ | |||||
'.font-condensed': { | |||||
'font-stretch': 'condensed', | |||||
}, | |||||
'.font-semi-condensed': { | |||||
'font-stretch': 'semi-condensed', | |||||
}, | |||||
'.font-expanded': { | |||||
'font-stretch': 'expanded', | |||||
}, | |||||
'.font-semi-expanded': { | |||||
'font-stretch': 'semi-expanded', | |||||
}, | |||||
'.font-inherit': { | |||||
'font-stretch': 'inherit', | |||||
}, | |||||
'.linejoin-round': { | |||||
'stroke-linejoin': 'round', | |||||
}, | |||||
'.linecap-round': { | |||||
'stroke-linecap': 'round', | |||||
}, | |||||
}); | |||||
}, | |||||
{ | |||||
theme: { | |||||
fontFamily: { | |||||
sans: ['var(--font-sans)', ...defaultTheme.fontFamily.sans], | |||||
headings: ['var(--font-headings)', ...defaultTheme.fontFamily.sans], | |||||
mono: ['var(--font-mono)', ...defaultTheme.fontFamily.mono], | |||||
inherit: ['inherit'], | |||||
}, | |||||
colors: { | |||||
'sidebar': 'rgb(var(--color-sidebar)', | |||||
'topbar': 'rgb(var(--color-topbar)', | |||||
'shade': 'rgb(var(--color-shade))', | |||||
'negative': 'rgb(var(--color-negative))', | |||||
'positive': 'rgb(var(--color-positive))', | |||||
'primary': 'hsl(var(--color-primary))', | |||||
'secondary': 'rgb(var(--color-secondary))', | |||||
'tertiary': 'rgb(var(--color-tertiary))', | |||||
'code-number': 'rgb(var(--color-code-number))', | |||||
'code-keyword': 'rgb(var(--color-code-keyword))', | |||||
'code-type': 'rgb(var(--color-code-type))', | |||||
'code-instance-attribute': 'rgb(var(--color-code-instance-attribute))', | |||||
'code-function': 'rgb(var(--color-code-function))', | |||||
'code-parameter': 'rgb(var(--color-code-parameter))', | |||||
'code-property': 'rgb(var(--color-code-property))', | |||||
'code-string': 'rgb(var(--color-code-string))', | |||||
'code-variable': 'rgb(var(--color-code-variable))', | |||||
'code-regexp': 'rgb(var(--color-code-regexp))', | |||||
'code-url': 'rgb(var(--color-code-url))', | |||||
'code-global': 'rgb(var(--color-code-global))', | |||||
'current': 'currentcolor', | |||||
'inherit': 'inherit', | |||||
'transparent': 'transparent', | |||||
}, | |||||
extend: { | |||||
fontSize: { | |||||
'lg': '1.125em', | |||||
'xl': '1.25em', | |||||
'2xl': '1.5em', | |||||
'3xl': '1.75em', | |||||
'4xl': '2em', | |||||
'5xl': '3em', | |||||
'6xl': '4em', | |||||
'xxs': '0.625rem', | |||||
}, | |||||
borderRadius: { | |||||
inherit: 'inherit', | |||||
}, | |||||
maxWidth: { | |||||
'1/3': '33.333333%', | |||||
}, | |||||
minWidth: { | |||||
6: '1.5rem', | |||||
10: '2.5rem', | |||||
12: '3rem', | |||||
16: '4rem', | |||||
48: '12rem', | |||||
64: '16rem', | |||||
32: '8rem', | |||||
}, | |||||
minHeight: { | |||||
6: '1.5rem', | |||||
10: '2.5rem', | |||||
12: '3rem', | |||||
16: '4rem', | |||||
64: '16rem', | |||||
}, | |||||
strokeWidth: { | |||||
3: '3', | |||||
}, | |||||
}, | |||||
}, | |||||
}, | |||||
); | |||||
const config: Config = { | |||||
content: [ | |||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}', | |||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}', | |||||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}', | |||||
'./node_modules/@tesseract-design/viewfinder-react/dist/**/*.{js,ts,jsx,tsx,mdx}', | |||||
'./node_modules/@tesseract-design/web-*-react/**/*.{js.ts,jsx,tsx,mdx}', | |||||
], | |||||
theme: { | |||||
extend: { | |||||
backgroundImage: { | |||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', | |||||
'gradient-conic': | |||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', | |||||
}, | |||||
}, | |||||
}, | |||||
plugins: [ | |||||
tailwind.plugin({ | |||||
}), | |||||
tesseractPlugin, | |||||
], | |||||
} | |||||
export default config |
@@ -0,0 +1,22 @@ | |||||
{ | |||||
"compilerOptions": { | |||||
"target": "es5", | |||||
"lib": ["dom", "dom.iterable", "esnext"], | |||||
"allowJs": true, | |||||
"skipLibCheck": true, | |||||
"strict": true, | |||||
"noEmit": true, | |||||
"esModuleInterop": true, | |||||
"module": "esnext", | |||||
"moduleResolution": "bundler", | |||||
"resolveJsonModule": true, | |||||
"isolatedModules": true, | |||||
"jsx": "preserve", | |||||
"incremental": true, | |||||
"paths": { | |||||
"@/*": ["./src/*"] | |||||
} | |||||
}, | |||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], | |||||
"exclude": ["node_modules"] | |||||
} |