init
79
CHANGELOG.md
Normal file
@ -0,0 +1,79 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-02-19
|
||||
|
||||
- Fix scroll issues related to the header navigation ([#1700](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1700))
|
||||
|
||||
## 2025-04-28
|
||||
|
||||
- Update template to Tailwind CSS v4.1.4
|
||||
|
||||
## 2025-04-10
|
||||
|
||||
- Update template to Tailwind CSS v4.1.3
|
||||
|
||||
## 2025-03-22
|
||||
|
||||
- Update template to Tailwind CSS v4.0.15
|
||||
|
||||
## 2025-02-10
|
||||
|
||||
- Update template to Tailwind CSS v4.0.6
|
||||
|
||||
## 2025-01-23
|
||||
|
||||
- Update template to Tailwind CSS v4.0
|
||||
|
||||
## 2024-06-18
|
||||
|
||||
- Update `prettier` and `prettier-plugin-tailwindcss` dependencies
|
||||
|
||||
## 2024-05-31
|
||||
|
||||
- Fix `npm audit` warnings
|
||||
|
||||
## 2024-05-07
|
||||
|
||||
- Bump Headless UI dependency to v2.0
|
||||
|
||||
## 2024-01-17
|
||||
|
||||
- Fix `shiki` dependency issues ([#1549](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1549))
|
||||
- Fix `sharp` dependency issues ([#1549](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1549))
|
||||
|
||||
## 2024-01-16
|
||||
|
||||
- Replace Twitter with X
|
||||
|
||||
## 2024-01-10
|
||||
|
||||
- Update Tailwind CSS, Next.js, Prettier, TypeScript, ESLint, and other dependencies
|
||||
|
||||
## 2023-09-07
|
||||
|
||||
- Added TypeScript version of template
|
||||
|
||||
## 2023-08-24
|
||||
|
||||
- Add missing `@types/mdx` dependency ([#1496](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1496))
|
||||
|
||||
## 2023-08-15
|
||||
|
||||
- Bump Next.js and MDX dependencies
|
||||
|
||||
## 2023-08-14
|
||||
|
||||
- Simplify article and case study metadata
|
||||
|
||||
## 2023-07-31
|
||||
|
||||
- Port template to Next.js app router
|
||||
- Fix route handlers with `.js` extensions ([#1484](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1484))
|
||||
|
||||
## 2023-07-26
|
||||
|
||||
- Add missing `acorn` and `acorn-jsx` dependencies ([#1481](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1481))
|
||||
|
||||
## 2023-07-13
|
||||
|
||||
- Initial release
|
36
README.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Studio
|
||||
|
||||
Studio is a [Tailwind Plus](https://tailwindcss.com/plus) site template built using [Tailwind CSS](https://tailwindcss.com) and [Next.js](https://nextjs.org).
|
||||
|
||||
## Getting started
|
||||
|
||||
To get started with this template, first install the npm dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Next, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website.
|
||||
|
||||
## Customizing
|
||||
|
||||
You can start editing this template by modifying the files in the `/src` folder. The site will auto-update as you edit these files.
|
||||
|
||||
## License
|
||||
|
||||
This site template is a commercial product and is licensed under the [Tailwind Plus license](https://tailwindcss.com/plus/license).
|
||||
|
||||
## Learn more
|
||||
|
||||
To learn more about the technologies used in this site template, see the following resources:
|
||||
|
||||
- [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation
|
||||
- [Next.js](https://nextjs.org/docs) - the official Next.js documentation
|
||||
- [Framer Motion](https://www.framer.com/docs/) - the official Framer Motion documentation
|
||||
- [MDX](https://mdxjs.com/) - the official MDX documentation
|
10
mdx-components.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { type MDXComponents as MDXComponentsType } from 'mdx/types'
|
||||
|
||||
import { MDXComponents } from '@/components/MDXComponents'
|
||||
|
||||
export function useMDXComponents(components: MDXComponentsType) {
|
||||
return {
|
||||
...components,
|
||||
...MDXComponents,
|
||||
}
|
||||
}
|
5
next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
83
next.config.mjs
Normal file
@ -0,0 +1,83 @@
|
||||
import rehypeShiki from '@leafac/rehype-shiki'
|
||||
import nextMDX from '@next/mdx'
|
||||
import { Parser } from 'acorn'
|
||||
import jsx from 'acorn-jsx'
|
||||
import escapeStringRegexp from 'escape-string-regexp'
|
||||
import * as path from 'path'
|
||||
import { recmaImportImages } from 'recma-import-images'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { remarkRehypeWrap } from 'remark-rehype-wrap'
|
||||
import remarkUnwrapImages from 'remark-unwrap-images'
|
||||
import shiki from 'shiki'
|
||||
import { unifiedConditional } from 'unified-conditional'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
|
||||
}
|
||||
|
||||
function remarkMDXLayout(source, metaName) {
|
||||
let parser = Parser.extend(jsx())
|
||||
let parseOptions = { ecmaVersion: 'latest', sourceType: 'module' }
|
||||
|
||||
return (tree) => {
|
||||
let imp = `import _Layout from '${source}'`
|
||||
let exp = `export default function Layout(props) {
|
||||
return <_Layout {...props} ${metaName}={${metaName}} />
|
||||
}`
|
||||
|
||||
tree.children.push(
|
||||
{
|
||||
type: 'mdxjsEsm',
|
||||
value: imp,
|
||||
data: { estree: parser.parse(imp, parseOptions) },
|
||||
},
|
||||
{
|
||||
type: 'mdxjsEsm',
|
||||
value: exp,
|
||||
data: { estree: parser.parse(exp, parseOptions) },
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default async function config() {
|
||||
let highlighter = await shiki.getHighlighter({
|
||||
theme: 'css-variables',
|
||||
})
|
||||
|
||||
let withMDX = nextMDX({
|
||||
extension: /\.mdx$/,
|
||||
options: {
|
||||
recmaPlugins: [recmaImportImages],
|
||||
rehypePlugins: [
|
||||
[rehypeShiki, { highlighter }],
|
||||
[
|
||||
remarkRehypeWrap,
|
||||
{
|
||||
node: { type: 'mdxJsxFlowElement', name: 'Typography' },
|
||||
start: ':root > :not(mdxJsxFlowElement)',
|
||||
end: ':root > mdxJsxFlowElement',
|
||||
},
|
||||
],
|
||||
],
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
remarkUnwrapImages,
|
||||
[
|
||||
unifiedConditional,
|
||||
[
|
||||
new RegExp(`^${escapeStringRegexp(path.resolve('src/app/blog'))}`),
|
||||
[[remarkMDXLayout, '@/app/blog/wrapper', 'article']],
|
||||
],
|
||||
[
|
||||
new RegExp(`^${escapeStringRegexp(path.resolve('src/app/work'))}`),
|
||||
[[remarkMDXLayout, '@/app/work/wrapper', 'caseStudy']],
|
||||
],
|
||||
],
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
return withMDX(nextConfig)
|
||||
}
|
8169
package-lock.json
generated
Normal file
47
package.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "tailwind-plus-studio",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"browserslist": "defaults, not ie <= 11",
|
||||
"dependencies": {
|
||||
"@leafac/rehype-shiki": "^2.2.1",
|
||||
"@mdx-js/loader": "^3.0.0",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@next/mdx": "^14.0.4",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@types/mdx": "^2.0.7",
|
||||
"@types/node": "^20.10.8",
|
||||
"@types/react": "^18.2.47",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"acorn": "^8.10.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"clsx": "^2.1.0",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"framer-motion": "^10.15.2",
|
||||
"next": "^14.0.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"recma-import-images": "0.0.3",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-rehype-wrap": "0.0.3",
|
||||
"remark-unwrap-images": "^4.0.0",
|
||||
"shiki": "^0.11.1",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"typescript": "^5.3.3",
|
||||
"unified-conditional": "0.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"sharp": "0.33.1"
|
||||
}
|
||||
}
|
5
postcss.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
7
prettier.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
/** @type {import('prettier').Options} */
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
plugins: ['prettier-plugin-tailwindcss'],
|
||||
tailwindStylesheet: './src/styles/tailwind.css',
|
||||
}
|
235
src/app/about/page.tsx
Normal file
@ -0,0 +1,235 @@
|
||||
import { type Metadata } from 'next'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { Border } from '@/components/Border'
|
||||
import { ContactSection } from '@/components/ContactSection'
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn, FadeInStagger } from '@/components/FadeIn'
|
||||
import { GridList, GridListItem } from '@/components/GridList'
|
||||
import { PageIntro } from '@/components/PageIntro'
|
||||
import { PageLinks } from '@/components/PageLinks'
|
||||
import { SectionIntro } from '@/components/SectionIntro'
|
||||
import { StatList, StatListItem } from '@/components/StatList'
|
||||
import imageAngelaFisher from '@/images/team/angela-fisher.jpg'
|
||||
import imageBenjaminRussel from '@/images/team/benjamin-russel.jpg'
|
||||
import imageBlakeReid from '@/images/team/blake-reid.jpg'
|
||||
import imageChelseaHagon from '@/images/team/chelsea-hagon.jpg'
|
||||
import imageDriesVincent from '@/images/team/dries-vincent.jpg'
|
||||
import imageEmmaDorsey from '@/images/team/emma-dorsey.jpg'
|
||||
import imageJeffreyWebb from '@/images/team/jeffrey-webb.jpg'
|
||||
import imageKathrynMurphy from '@/images/team/kathryn-murphy.jpg'
|
||||
import imageLeonardKrasner from '@/images/team/leonard-krasner.jpg'
|
||||
import imageLeslieAlexander from '@/images/team/leslie-alexander.jpg'
|
||||
import imageMichaelFoster from '@/images/team/michael-foster.jpg'
|
||||
import imageWhitneyFrancis from '@/images/team/whitney-francis.jpg'
|
||||
import { loadArticles } from '@/lib/mdx'
|
||||
import { RootLayout } from '@/components/RootLayout'
|
||||
|
||||
function Culture() {
|
||||
return (
|
||||
<div className="mt-24 rounded-4xl bg-neutral-950 py-24 sm:mt-32 lg:mt-40 lg:py-32">
|
||||
<SectionIntro
|
||||
eyebrow="Our culture"
|
||||
title="Balance your passion with your passion for life."
|
||||
invert
|
||||
>
|
||||
<p>
|
||||
We are a group of like-minded people who share the same core values.
|
||||
</p>
|
||||
</SectionIntro>
|
||||
<Container className="mt-16">
|
||||
<GridList>
|
||||
<GridListItem title="Loyalty" invert>
|
||||
Our team has been with us since the beginning because none of them
|
||||
are allowed to have LinkedIn profiles.
|
||||
</GridListItem>
|
||||
<GridListItem title="Trust" invert>
|
||||
We don’t care when our team works just as long as they are working
|
||||
every waking second.
|
||||
</GridListItem>
|
||||
<GridListItem title="Compassion" invert>
|
||||
You never know what someone is going through at home and we make
|
||||
sure to never find out.
|
||||
</GridListItem>
|
||||
</GridList>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const team = [
|
||||
{
|
||||
title: 'Leadership',
|
||||
people: [
|
||||
{
|
||||
name: 'Leslie Alexander',
|
||||
role: 'Co-Founder / CEO',
|
||||
image: { src: imageLeslieAlexander },
|
||||
},
|
||||
{
|
||||
name: 'Michael Foster',
|
||||
role: 'Co-Founder / CTO',
|
||||
image: { src: imageMichaelFoster },
|
||||
},
|
||||
{
|
||||
name: 'Dries Vincent',
|
||||
role: 'Partner & Business Relations',
|
||||
image: { src: imageDriesVincent },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Team',
|
||||
people: [
|
||||
{
|
||||
name: 'Chelsea Hagon',
|
||||
role: 'Senior Developer',
|
||||
image: { src: imageChelseaHagon },
|
||||
},
|
||||
{
|
||||
name: 'Emma Dorsey',
|
||||
role: 'Senior Designer',
|
||||
image: { src: imageEmmaDorsey },
|
||||
},
|
||||
{
|
||||
name: 'Leonard Krasner',
|
||||
role: 'VP, User Experience',
|
||||
image: { src: imageLeonardKrasner },
|
||||
},
|
||||
{
|
||||
name: 'Blake Reid',
|
||||
role: 'Junior Copywriter',
|
||||
image: { src: imageBlakeReid },
|
||||
},
|
||||
{
|
||||
name: 'Kathryn Murphy',
|
||||
role: 'VP, Human Resources',
|
||||
image: { src: imageKathrynMurphy },
|
||||
},
|
||||
{
|
||||
name: 'Whitney Francis',
|
||||
role: 'Content Specialist',
|
||||
image: { src: imageWhitneyFrancis },
|
||||
},
|
||||
{
|
||||
name: 'Jeffrey Webb',
|
||||
role: 'Account Coordinator',
|
||||
image: { src: imageJeffreyWebb },
|
||||
},
|
||||
{
|
||||
name: 'Benjamin Russel',
|
||||
role: 'Senior Developer',
|
||||
image: { src: imageBenjaminRussel },
|
||||
},
|
||||
{
|
||||
name: 'Angela Fisher',
|
||||
role: 'Front-end Developer',
|
||||
image: { src: imageAngelaFisher },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function Team() {
|
||||
return (
|
||||
<Container className="mt-24 sm:mt-32 lg:mt-40">
|
||||
<div className="space-y-24">
|
||||
{team.map((group) => (
|
||||
<FadeInStagger key={group.title}>
|
||||
<Border as={FadeIn} />
|
||||
<div className="grid grid-cols-1 gap-6 pt-12 sm:pt-16 lg:grid-cols-4 xl:gap-8">
|
||||
<FadeIn>
|
||||
<h2 className="font-display text-2xl font-semibold text-neutral-950">
|
||||
{group.title}
|
||||
</h2>
|
||||
</FadeIn>
|
||||
<div className="lg:col-span-3">
|
||||
<ul
|
||||
role="list"
|
||||
className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:gap-8"
|
||||
>
|
||||
{group.people.map((person) => (
|
||||
<li key={person.name}>
|
||||
<FadeIn>
|
||||
<div className="group relative overflow-hidden rounded-3xl bg-neutral-100">
|
||||
<Image
|
||||
alt=""
|
||||
{...person.image}
|
||||
className="h-96 w-full object-cover grayscale transition duration-500 motion-safe:group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 flex flex-col justify-end bg-linear-to-t from-black to-black/0 to-40% p-6">
|
||||
<p className="font-display text-base/6 font-semibold tracking-wide text-white">
|
||||
{person.name}
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-white">
|
||||
{person.role}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</FadeIn>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInStagger>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'About Us',
|
||||
description:
|
||||
'We believe that our strength lies in our collaborative approach, which puts our clients at the center of everything we do.',
|
||||
}
|
||||
|
||||
export default async function About() {
|
||||
let blogArticles = (await loadArticles()).slice(0, 2)
|
||||
|
||||
return (
|
||||
<RootLayout>
|
||||
<PageIntro eyebrow="About us" title="Our strength is collaboration">
|
||||
<p>
|
||||
We believe that our strength lies in our collaborative approach, which
|
||||
puts our clients at the center of everything we do.
|
||||
</p>
|
||||
<div className="mt-10 max-w-2xl space-y-6 text-base">
|
||||
<p>
|
||||
Studio was started by three friends who noticed that developer
|
||||
studios were charging clients double what an in-house team would
|
||||
cost. Since the beginning, we have been committed to doing things
|
||||
differently by charging triple instead.
|
||||
</p>
|
||||
<p>
|
||||
At Studio, we’re more than just colleagues — we’re a family. This
|
||||
means we pay very little and expect people to work late. We want our
|
||||
employees to bring their whole selves to work. In return, we just
|
||||
ask that they keep themselves there until at least 6:30pm.
|
||||
</p>
|
||||
</div>
|
||||
</PageIntro>
|
||||
<Container className="mt-16">
|
||||
<StatList>
|
||||
<StatListItem value="35" label="Underpaid employees" />
|
||||
<StatListItem value="52" label="Placated clients" />
|
||||
<StatListItem value="$25M" label="Invoices billed" />
|
||||
</StatList>
|
||||
</Container>
|
||||
|
||||
<Culture />
|
||||
|
||||
<Team />
|
||||
|
||||
<PageLinks
|
||||
className="mt-24 sm:mt-32 lg:mt-40"
|
||||
title="From the blog"
|
||||
intro="Our team of experienced designers and developers has just one thing on their mind; working on your ideas to draw a smile on the face of your users worldwide. From conducting Brand Sprints to UX Design."
|
||||
pages={blogArticles}
|
||||
/>
|
||||
|
||||
<ContactSection />
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
After Width: | Height: | Size: 396 KiB |
After Width: | Height: | Size: 346 KiB |
@ -0,0 +1,50 @@
|
||||
import imageLeslieAlexander from '@/images/team/leslie-alexander.jpg'
|
||||
|
||||
export const article = {
|
||||
date: '2023-02-18',
|
||||
title: '3 Lessons We Learned Going Back to the Office',
|
||||
description:
|
||||
'Earlier this year we made the bold decision to make everyone come back to the office full-time after two years working from a dressing table in the corner of their bedroom.',
|
||||
author: {
|
||||
name: 'Leslie Alexander',
|
||||
role: 'Co-Founder / CEO',
|
||||
image: { src: imageLeslieAlexander },
|
||||
},
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: article.title,
|
||||
description: article.description,
|
||||
}
|
||||
|
||||
## 1. Efficiency is Hard to Measure
|
||||
|
||||
Although almost every practical measure of our productivity decreased significantly after returning to the office, as a management team we felt this incredible uptick in energy. We realised that there is an intangible benefit to seeing everyone’s screen at all times, that isn’t easily measurable in numbers.
|
||||
|
||||
Sure, we tried to recreate this feeling during our remote days with employee monitoring software but we always had this nagging doubt that our developers had hacked their way around it.
|
||||
|
||||
<TopTip>
|
||||
Getting one of those old-timey punch clocks is a great way to monitor
|
||||
attendance while maintaining a fun atmosphere. Expect to hear things like
|
||||
“Back at the coalface today!”.
|
||||
</TopTip>
|
||||
|
||||

|
||||
|
||||
## 2. Turnover: a Fresh Perspective
|
||||
|
||||
We parted ways with almost all of our senior development team within the first month of going back to the office, due to some irreconcilable differences. Stressed and worried, we decided to try turn this into a positive.
|
||||
|
||||
Luckily for us, it was the same week that CoPilot launched and we were able to replace everyone with five bootcamp graduates all logged into one Github account.
|
||||
|
||||
We have been consistently surprised at the fresh energy these new grads brought to our organisation and have since vowed to never hire anyone with more than 3 months experience again.
|
||||
|
||||

|
||||
|
||||
## 3. Cost Efficiency
|
||||
|
||||
Demand is at an all time low for commercial real-estate, which means it’s never been more affordable to cram forty people into an open plan office.
|
||||
|
||||
What’s more, is we’ve found that we can offer extremely low-cost perks like a snack cupboard or free beer in-lieu of higher salaries. For every foosball table we buy, we find we can offer around 5% less salary per job posting. Our full-time barista is the highest paid employee, after management.
|
||||
|
||||

|
After Width: | Height: | Size: 399 KiB |
After Width: | Height: | Size: 558 KiB |
After Width: | Height: | Size: 440 KiB |
42
src/app/blog/a-short-guide-to-component-naming/page.mdx
Normal file
@ -0,0 +1,42 @@
|
||||
import imageAngelaFisher from '@/images/team/angela-fisher.jpg'
|
||||
|
||||
export const article = {
|
||||
date: '2022-12-01',
|
||||
title: 'A Short Guide to Component Naming',
|
||||
description:
|
||||
'As a developer, the most important aspect of your job is naming components. It’s not just about being descriptive and clear, but also about having fun and being creative.',
|
||||
author: {
|
||||
name: 'Angela Fisher',
|
||||
role: 'Front-end Developer',
|
||||
image: { src: imageAngelaFisher },
|
||||
},
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: article.title,
|
||||
description: article.description,
|
||||
}
|
||||
|
||||
## 1. Brevity is Key
|
||||
|
||||
Time is scarce, don’t waste it typing out long, descriptive component names. One approach is to give them short, cryptic names that only you will understand.
|
||||
|
||||
Need a button? Call it "btn". A modal? How about "md"? You’ll save precious minutes per day and you’ll get the added benefit of being the only person in the codebase who knows where anything is. This is called job security.
|
||||
|
||||

|
||||
|
||||
## 2. Rank High in Search
|
||||
|
||||
When working in large repos with lots of collaborators, it’s important that your component ranks high when people search for anything.
|
||||
|
||||
One way to stand out is to include all the possible search terms in your component name. Instead of “SignInButton” you might want call it “SignInButtonAuthenticationCookieUserLogIn” which will ensure that it is returned in almost any related search result.
|
||||
|
||||

|
||||
|
||||
## 3. Mix Languages
|
||||
|
||||
If you work remotely, it’s likely you are on a global team and yet all your components have English names. This slows down your non-english colleagues considerably so you should allow them to use their native tongue when naming components.
|
||||
|
||||
You can create an index file that maps all the different languages within your repo. Need a dropdown? Look for “Desplegable”. A form? Search “Форма”. You’ll learn multiple new languages while being more inclusive to your colleagues.
|
||||
|
||||

|
BIN
src/app/blog/a-short-guide-to-component-naming/typewriter.jpg
Normal file
After Width: | Height: | Size: 274 KiB |
BIN
src/app/blog/future-of-web-development/laptop.jpg
Normal file
After Width: | Height: | Size: 217 KiB |
48
src/app/blog/future-of-web-development/page.mdx
Normal file
@ -0,0 +1,48 @@
|
||||
import imageChelseaHagon from '@/images/team/chelsea-hagon.jpg'
|
||||
|
||||
export const article = {
|
||||
date: '2023-04-06',
|
||||
title: 'The Future of Web Development: Our Predictions for 2023',
|
||||
description:
|
||||
'Let’s explore the latest trends in web development, and regurgitate some predictions we read on X for how they will shape the industry in the coming year.',
|
||||
author: {
|
||||
name: 'Chelsea Hagon',
|
||||
role: 'Senior Developer',
|
||||
image: { src: imageChelseaHagon },
|
||||
},
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: article.title,
|
||||
description: article.description,
|
||||
}
|
||||
|
||||
## 1. AI Assisted Development
|
||||
|
||||
With the launch of Github Copilot in 2022 the industry got its first glimpse at what it would look like to have Stack Overflow plumbed straight into your IDE. Copilot has given thousands of developers what they always longed for: plausible deniability over the bugs they write.
|
||||
|
||||

|
||||
|
||||
In 2023 we can expect these assistants to become more sophisticated and for that to have ripple effects throughout the industry.
|
||||
|
||||
We predict that traffic to MDN will decline precipitously as developers realise they no longer need to look up JS array methods. We also expect Stack Overflow’s sister site, Prompt Overflow, to become one of the most popular sites on the internet in a matter of months.
|
||||
|
||||
## 2. Rendering Patterns
|
||||
|
||||
To server render or not to server render? In 2022 the owners of the internet, Vercel, decided that instead of making this choice once for your whole application, now you will need to decide every time you write a new component.
|
||||
|
||||
Because front-end development was becoming too easy, the same people who write CSS will now need to know how Streaming SSR and Progressive Hydration work.
|
||||
|
||||

|
||||
|
||||
In 2023 we can expect frameworks to adopt increasingly granular rendering patterns culminating in per-line rendering (PLR) later this year. We can also expect job postings for Rendering Reliability Engineers to reach an all time high.
|
||||
|
||||
## 3. JS Runtimes
|
||||
|
||||
Because choosing a JS runtime was one of the only areas where a developer wasn’t paralysed with choice, in early 2020, the creator of Node gave us something new to agonise over. The launch of Deno and Bun heralded the final mutation of JavaScript into a language that can truly run anywhere it wasn’t intended to.
|
||||
|
||||
These new JS runtimes mean we can now serve HTML faster than ever before. For example, we’ve reduced the Time to First Byte (TTFB) of this blog to -0.4s. That means it actually loaded before you clicked the link.
|
||||
|
||||

|
||||
|
||||
In 2023 we can expect even faster and more specialised JS runtimes to launch, including the promising Boil, a runtime specifically designed to reduce cold boot times on WiFi enabled kettles. All of these advancements promise to make the future of botnets a truly exciting one.
|
BIN
src/app/blog/future-of-web-development/pilot.jpg
Normal file
After Width: | Height: | Size: 415 KiB |
BIN
src/app/blog/future-of-web-development/server.jpg
Normal file
After Width: | Height: | Size: 244 KiB |
90
src/app/blog/page.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { type Metadata } from 'next'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Border } from '@/components/Border'
|
||||
import { Button } from '@/components/Button'
|
||||
import { ContactSection } from '@/components/ContactSection'
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn } from '@/components/FadeIn'
|
||||
import { PageIntro } from '@/components/PageIntro'
|
||||
import { RootLayout } from '@/components/RootLayout'
|
||||
import { formatDate } from '@/lib/formatDate'
|
||||
import { loadArticles } from '@/lib/mdx'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Blog',
|
||||
description:
|
||||
'Stay up-to-date with the latest industry news as our marketing teams finds new ways to re-purpose old CSS tricks articles.',
|
||||
}
|
||||
|
||||
export default async function Blog() {
|
||||
let articles = await loadArticles()
|
||||
|
||||
return (
|
||||
<RootLayout>
|
||||
<PageIntro eyebrow="Blog" title="The latest articles and news">
|
||||
<p>
|
||||
Stay up-to-date with the latest industry news as our marketing teams
|
||||
finds new ways to re-purpose old CSS tricks articles.
|
||||
</p>
|
||||
</PageIntro>
|
||||
|
||||
<Container className="mt-24 sm:mt-32 lg:mt-40">
|
||||
<div className="space-y-24 lg:space-y-32">
|
||||
{articles.map((article) => (
|
||||
<FadeIn key={article.href}>
|
||||
<article>
|
||||
<Border className="pt-16">
|
||||
<div className="relative lg:-mx-4 lg:flex lg:justify-end">
|
||||
<div className="pt-10 lg:w-2/3 lg:flex-none lg:px-4 lg:pt-0">
|
||||
<h2 className="font-display text-2xl font-semibold text-neutral-950">
|
||||
<Link href={article.href}>{article.title}</Link>
|
||||
</h2>
|
||||
<dl className="lg:absolute lg:top-0 lg:left-0 lg:w-1/3 lg:px-4">
|
||||
<dt className="sr-only">Published</dt>
|
||||
<dd className="absolute top-0 left-0 text-sm text-neutral-950 lg:static">
|
||||
<time dateTime={article.date}>
|
||||
{formatDate(article.date)}
|
||||
</time>
|
||||
</dd>
|
||||
<dt className="sr-only">Author</dt>
|
||||
<dd className="mt-6 flex gap-x-4">
|
||||
<div className="flex-none overflow-hidden rounded-xl bg-neutral-100">
|
||||
<Image
|
||||
alt=""
|
||||
{...article.author.image}
|
||||
className="h-12 w-12 object-cover grayscale"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-neutral-950">
|
||||
<div className="font-semibold">
|
||||
{article.author.name}
|
||||
</div>
|
||||
<div>{article.author.role}</div>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
<p className="mt-6 max-w-2xl text-base text-neutral-600">
|
||||
{article.description}
|
||||
</p>
|
||||
<Button
|
||||
href={article.href}
|
||||
aria-label={`Read more: ${article.title}`}
|
||||
className="mt-8"
|
||||
>
|
||||
Read more
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Border>
|
||||
</article>
|
||||
</FadeIn>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<ContactSection />
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
60
src/app/blog/wrapper.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { ContactSection } from '@/components/ContactSection'
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn } from '@/components/FadeIn'
|
||||
import { MDXComponents } from '@/components/MDXComponents'
|
||||
import { PageLinks } from '@/components/PageLinks'
|
||||
import { RootLayout } from '@/components/RootLayout'
|
||||
import { formatDate } from '@/lib/formatDate'
|
||||
import { type Article, type MDXEntry, loadArticles } from '@/lib/mdx'
|
||||
|
||||
export default async function BlogArticleWrapper({
|
||||
article,
|
||||
children,
|
||||
}: {
|
||||
article: MDXEntry<Article>
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
let allArticles = await loadArticles()
|
||||
let moreArticles = allArticles
|
||||
.filter(({ metadata }) => metadata !== article)
|
||||
.slice(0, 2)
|
||||
|
||||
return (
|
||||
<RootLayout>
|
||||
<Container as="article" className="mt-24 sm:mt-32 lg:mt-40">
|
||||
<FadeIn>
|
||||
<header className="mx-auto flex max-w-5xl flex-col text-center">
|
||||
<h1 className="mt-6 font-display text-5xl font-medium tracking-tight text-balance text-neutral-950 sm:text-6xl">
|
||||
{article.title}
|
||||
</h1>
|
||||
<time
|
||||
dateTime={article.date}
|
||||
className="order-first text-sm text-neutral-950"
|
||||
>
|
||||
{formatDate(article.date)}
|
||||
</time>
|
||||
<p className="mt-6 text-sm font-semibold text-neutral-950">
|
||||
by {article.author.name}, {article.author.role}
|
||||
</p>
|
||||
</header>
|
||||
</FadeIn>
|
||||
|
||||
<FadeIn>
|
||||
<MDXComponents.wrapper className="mt-24 sm:mt-32 lg:mt-40">
|
||||
{children}
|
||||
</MDXComponents.wrapper>
|
||||
</FadeIn>
|
||||
</Container>
|
||||
|
||||
{moreArticles.length > 0 && (
|
||||
<PageLinks
|
||||
className="mt-24 sm:mt-32 lg:mt-40"
|
||||
title="More articles"
|
||||
pages={moreArticles}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ContactSection />
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
164
src/app/contact/page.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import { useId } from 'react'
|
||||
import { type Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Border } from '@/components/Border'
|
||||
import { Button } from '@/components/Button'
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn } from '@/components/FadeIn'
|
||||
import { Offices } from '@/components/Offices'
|
||||
import { PageIntro } from '@/components/PageIntro'
|
||||
import { SocialMedia } from '@/components/SocialMedia'
|
||||
import { RootLayout } from '@/components/RootLayout'
|
||||
|
||||
function TextInput({
|
||||
label,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'input'> & { label: string }) {
|
||||
let id = useId()
|
||||
|
||||
return (
|
||||
<div className="group relative z-0 transition-all focus-within:z-10">
|
||||
<input
|
||||
type="text"
|
||||
id={id}
|
||||
{...props}
|
||||
placeholder=" "
|
||||
className="peer block w-full border border-neutral-300 bg-transparent px-6 pt-12 pb-4 text-base/6 text-neutral-950 ring-4 ring-transparent transition group-first:rounded-t-2xl group-last:rounded-b-2xl focus:border-neutral-950 focus:ring-neutral-950/5 focus:outline-hidden"
|
||||
/>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="pointer-events-none absolute top-1/2 left-6 -mt-3 origin-left text-base/6 text-neutral-500 transition-all duration-200 peer-not-placeholder-shown:-translate-y-4 peer-not-placeholder-shown:scale-75 peer-not-placeholder-shown:font-semibold peer-not-placeholder-shown:text-neutral-950 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-semibold peer-focus:text-neutral-950"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioInput({
|
||||
label,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'input'> & { label: string }) {
|
||||
return (
|
||||
<label className="flex gap-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
{...props}
|
||||
className="h-6 w-6 flex-none appearance-none rounded-full border border-neutral-950/20 outline-hidden checked:border-[0.5rem] checked:border-neutral-950 focus-visible:ring-1 focus-visible:ring-neutral-950 focus-visible:ring-offset-2"
|
||||
/>
|
||||
<span className="text-base/6 text-neutral-950">{label}</span>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function ContactForm() {
|
||||
return (
|
||||
<FadeIn className="lg:order-last">
|
||||
<form>
|
||||
<h2 className="font-display text-base font-semibold text-neutral-950">
|
||||
Work inquiries
|
||||
</h2>
|
||||
<div className="isolate mt-6 -space-y-px rounded-2xl bg-white/50">
|
||||
<TextInput label="Name" name="name" autoComplete="name" />
|
||||
<TextInput
|
||||
label="Email"
|
||||
type="email"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
/>
|
||||
<TextInput
|
||||
label="Company"
|
||||
name="company"
|
||||
autoComplete="organization"
|
||||
/>
|
||||
<TextInput label="Phone" type="tel" name="phone" autoComplete="tel" />
|
||||
<TextInput label="Message" name="message" />
|
||||
<div className="border border-neutral-300 px-6 py-8 first:rounded-t-2xl last:rounded-b-2xl">
|
||||
<fieldset>
|
||||
<legend className="text-base/6 text-neutral-500">Budget</legend>
|
||||
<div className="mt-6 grid grid-cols-1 gap-8 sm:grid-cols-2">
|
||||
<RadioInput label="$25K – $50K" name="budget" value="25" />
|
||||
<RadioInput label="$50K – $100K" name="budget" value="50" />
|
||||
<RadioInput label="$100K – $150K" name="budget" value="100" />
|
||||
<RadioInput label="More than $150K" name="budget" value="150" />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" className="mt-10">
|
||||
Let’s work together
|
||||
</Button>
|
||||
</form>
|
||||
</FadeIn>
|
||||
)
|
||||
}
|
||||
|
||||
function ContactDetails() {
|
||||
return (
|
||||
<FadeIn>
|
||||
<h2 className="font-display text-base font-semibold text-neutral-950">
|
||||
Our offices
|
||||
</h2>
|
||||
<p className="mt-6 text-base text-neutral-600">
|
||||
Prefer doing things in person? We don’t but we have to list our
|
||||
addresses here for legal reasons.
|
||||
</p>
|
||||
|
||||
<Offices className="mt-10 grid grid-cols-1 gap-8 sm:grid-cols-2" />
|
||||
|
||||
<Border className="mt-16 pt-16">
|
||||
<h2 className="font-display text-base font-semibold text-neutral-950">
|
||||
Email us
|
||||
</h2>
|
||||
<dl className="mt-6 grid grid-cols-1 gap-8 text-sm sm:grid-cols-2">
|
||||
{[
|
||||
['Careers', 'careers@studioagency.com'],
|
||||
['Press', 'press@studioagency.com'],
|
||||
].map(([label, email]) => (
|
||||
<div key={email}>
|
||||
<dt className="font-semibold text-neutral-950">{label}</dt>
|
||||
<dd>
|
||||
<Link
|
||||
href={`mailto:${email}`}
|
||||
className="text-neutral-600 hover:text-neutral-950"
|
||||
>
|
||||
{email}
|
||||
</Link>
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</Border>
|
||||
|
||||
<Border className="mt-16 pt-16">
|
||||
<h2 className="font-display text-base font-semibold text-neutral-950">
|
||||
Follow us
|
||||
</h2>
|
||||
<SocialMedia className="mt-6" />
|
||||
</Border>
|
||||
</FadeIn>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Contact Us',
|
||||
description: 'Let’s work together. We can’t wait to hear from you.',
|
||||
}
|
||||
|
||||
export default function Contact() {
|
||||
return (
|
||||
<RootLayout>
|
||||
<PageIntro eyebrow="Contact us" title="Let’s work together">
|
||||
<p>We can’t wait to hear from you.</p>
|
||||
</PageIntro>
|
||||
|
||||
<Container className="mt-24 sm:mt-32 lg:mt-40">
|
||||
<div className="grid grid-cols-1 gap-x-8 gap-y-24 lg:grid-cols-2">
|
||||
<ContactForm />
|
||||
<ContactDetails />
|
||||
</div>
|
||||
</Container>
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
BIN
src/app/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
18
src/app/layout.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { type Metadata } from 'next'
|
||||
|
||||
import '@/styles/tailwind.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '%s - Studio',
|
||||
default: 'Studio - Award winning developer studio based in Denmark',
|
||||
},
|
||||
}
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className="h-full bg-neutral-950 text-base antialiased">
|
||||
<body className="flex min-h-full flex-col">{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
28
src/app/not-found.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn } from '@/components/FadeIn'
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<Container className="flex h-full items-center pt-24 sm:pt-32 lg:pt-40">
|
||||
<FadeIn className="flex max-w-xl flex-col items-center text-center">
|
||||
<p className="font-display text-4xl font-semibold text-neutral-950 sm:text-5xl">
|
||||
404
|
||||
</p>
|
||||
<h1 className="mt-4 font-display text-2xl font-semibold text-neutral-950">
|
||||
Page not found
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-neutral-600">
|
||||
Sorry, we couldn’t find the page you’re looking for.
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-4 text-sm font-semibold text-neutral-950 transition hover:text-neutral-700"
|
||||
>
|
||||
Go to the home page
|
||||
</Link>
|
||||
</FadeIn>
|
||||
</Container>
|
||||
)
|
||||
}
|
217
src/app/page.tsx
Normal file
@ -0,0 +1,217 @@
|
||||
import { type Metadata } from 'next'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { ContactSection } from '@/components/ContactSection'
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn, FadeInStagger } from '@/components/FadeIn'
|
||||
import { List, ListItem } from '@/components/List'
|
||||
import { SectionIntro } from '@/components/SectionIntro'
|
||||
import { StylizedImage } from '@/components/StylizedImage'
|
||||
import { Testimonial } from '@/components/Testimonial'
|
||||
import logoBrightPath from '@/images/clients/bright-path/logo-light.svg'
|
||||
import logoFamilyFund from '@/images/clients/family-fund/logo-light.svg'
|
||||
import logoGreenLife from '@/images/clients/green-life/logo-light.svg'
|
||||
import logoHomeWork from '@/images/clients/home-work/logo-light.svg'
|
||||
import logoMailSmirk from '@/images/clients/mail-smirk/logo-light.svg'
|
||||
import logoNorthAdventures from '@/images/clients/north-adventures/logo-light.svg'
|
||||
import logoPhobiaDark from '@/images/clients/phobia/logo-dark.svg'
|
||||
import logoPhobiaLight from '@/images/clients/phobia/logo-light.svg'
|
||||
import logoUnseal from '@/images/clients/unseal/logo-light.svg'
|
||||
import imageLaptop from '@/images/laptop.jpg'
|
||||
import { type CaseStudy, type MDXEntry, loadCaseStudies } from '@/lib/mdx'
|
||||
import { RootLayout } from '@/components/RootLayout'
|
||||
|
||||
const clients = [
|
||||
['Phobia', logoPhobiaLight],
|
||||
['Family Fund', logoFamilyFund],
|
||||
['Unseal', logoUnseal],
|
||||
['Mail Smirk', logoMailSmirk],
|
||||
['Home Work', logoHomeWork],
|
||||
['Green Life', logoGreenLife],
|
||||
['Bright Path', logoBrightPath],
|
||||
['North Adventures', logoNorthAdventures],
|
||||
]
|
||||
|
||||
function Clients() {
|
||||
return (
|
||||
<div className="mt-24 rounded-4xl bg-neutral-950 py-20 sm:mt-32 sm:py-32 lg:mt-56">
|
||||
<Container>
|
||||
<FadeIn className="flex items-center gap-x-8">
|
||||
<h2 className="text-center font-display text-sm font-semibold tracking-wider text-white sm:text-left">
|
||||
We’ve worked with hundreds of amazing people
|
||||
</h2>
|
||||
<div className="h-px flex-auto bg-neutral-800" />
|
||||
</FadeIn>
|
||||
<FadeInStagger faster>
|
||||
<ul
|
||||
role="list"
|
||||
className="mt-10 grid grid-cols-2 gap-x-8 gap-y-10 lg:grid-cols-4"
|
||||
>
|
||||
{clients.map(([client, logo]) => (
|
||||
<li key={client}>
|
||||
<FadeIn>
|
||||
<Image src={logo} alt={client} unoptimized />
|
||||
</FadeIn>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</FadeInStagger>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CaseStudies({
|
||||
caseStudies,
|
||||
}: {
|
||||
caseStudies: Array<MDXEntry<CaseStudy>>
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<SectionIntro
|
||||
title="Harnessing technology for a brighter future"
|
||||
className="mt-24 sm:mt-32 lg:mt-40"
|
||||
>
|
||||
<p>
|
||||
We believe technology is the answer to the world’s greatest
|
||||
challenges. It’s also the cause, so we find ourselves in bit of a
|
||||
catch 22 situation.
|
||||
</p>
|
||||
</SectionIntro>
|
||||
<Container className="mt-16">
|
||||
<FadeInStagger className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
||||
{caseStudies.map((caseStudy) => (
|
||||
<FadeIn key={caseStudy.href} className="flex">
|
||||
<article className="relative flex w-full flex-col rounded-3xl p-6 ring-1 ring-neutral-950/5 transition hover:bg-neutral-50 sm:p-8">
|
||||
<h3>
|
||||
<Link href={caseStudy.href}>
|
||||
<span className="absolute inset-0 rounded-3xl" />
|
||||
<Image
|
||||
src={caseStudy.logo}
|
||||
alt={caseStudy.client}
|
||||
className="h-16 w-16"
|
||||
unoptimized
|
||||
/>
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="mt-6 flex gap-x-2 text-sm text-neutral-950">
|
||||
<time
|
||||
dateTime={caseStudy.date.split('-')[0]}
|
||||
className="font-semibold"
|
||||
>
|
||||
{caseStudy.date.split('-')[0]}
|
||||
</time>
|
||||
<span className="text-neutral-300" aria-hidden="true">
|
||||
/
|
||||
</span>
|
||||
<span>Case study</span>
|
||||
</p>
|
||||
<p className="mt-6 font-display text-2xl font-semibold text-neutral-950">
|
||||
{caseStudy.title}
|
||||
</p>
|
||||
<p className="mt-4 text-base text-neutral-600">
|
||||
{caseStudy.description}
|
||||
</p>
|
||||
</article>
|
||||
</FadeIn>
|
||||
))}
|
||||
</FadeInStagger>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Services() {
|
||||
return (
|
||||
<>
|
||||
<SectionIntro
|
||||
eyebrow="Services"
|
||||
title="We help you identify, explore and respond to new opportunities."
|
||||
className="mt-24 sm:mt-32 lg:mt-40"
|
||||
>
|
||||
<p>
|
||||
As long as those opportunities involve giving us money to re-purpose
|
||||
old projects — we can come up with an endless number of those.
|
||||
</p>
|
||||
</SectionIntro>
|
||||
<Container className="mt-16">
|
||||
<div className="lg:flex lg:items-center lg:justify-end">
|
||||
<div className="flex justify-center lg:w-1/2 lg:justify-end lg:pr-12">
|
||||
<FadeIn className="w-135 flex-none lg:w-180">
|
||||
<StylizedImage
|
||||
src={imageLaptop}
|
||||
sizes="(min-width: 1024px) 41rem, 31rem"
|
||||
className="justify-center lg:justify-end"
|
||||
/>
|
||||
</FadeIn>
|
||||
</div>
|
||||
<List className="mt-16 lg:mt-0 lg:w-1/2 lg:min-w-132 lg:pl-4">
|
||||
<ListItem title="Web development">
|
||||
We specialise in crafting beautiful, high quality marketing pages.
|
||||
The rest of the website will be a shell that uses lorem ipsum
|
||||
everywhere.
|
||||
</ListItem>
|
||||
<ListItem title="Application development">
|
||||
We have a team of skilled developers who are experts in the latest
|
||||
app frameworks, like Angular 1 and Google Web Toolkit.
|
||||
</ListItem>
|
||||
<ListItem title="E-commerce">
|
||||
We are at the forefront of modern e-commerce development. Which
|
||||
mainly means adding your logo to the Shopify store template we’ve
|
||||
used for the past six years.
|
||||
</ListItem>
|
||||
<ListItem title="Custom content management">
|
||||
At Studio we understand the importance of having a robust and
|
||||
customised CMS. That’s why we run all of our client projects out
|
||||
of a single, enormous Joomla instance.
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
'We are a development studio working at the intersection of design and technology.',
|
||||
}
|
||||
|
||||
export default async function Home() {
|
||||
let caseStudies = (await loadCaseStudies()).slice(0, 3)
|
||||
|
||||
return (
|
||||
<RootLayout>
|
||||
<Container className="mt-24 sm:mt-32 md:mt-56">
|
||||
<FadeIn className="max-w-3xl">
|
||||
<h1 className="font-display text-5xl font-medium tracking-tight text-balance text-neutral-950 sm:text-7xl">
|
||||
Award-winning development studio based in Denmark.
|
||||
</h1>
|
||||
<p className="mt-6 text-xl text-neutral-600">
|
||||
We are a development studio working at the intersection of design
|
||||
and technology. It’s a really busy intersection though — a lot of
|
||||
our staff have been involved in hit and runs.
|
||||
</p>
|
||||
</FadeIn>
|
||||
</Container>
|
||||
|
||||
<Clients />
|
||||
|
||||
<CaseStudies caseStudies={caseStudies} />
|
||||
|
||||
<Testimonial
|
||||
className="mt-24 sm:mt-32 lg:mt-40"
|
||||
client={{ name: 'Phobia', logo: logoPhobiaDark }}
|
||||
>
|
||||
The team at Studio went above and beyond with our onboarding, even
|
||||
finding a way to access the user’s microphone without triggering one of
|
||||
those annoying permission dialogs.
|
||||
</Testimonial>
|
||||
|
||||
<Services />
|
||||
|
||||
<ContactSection />
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
271
src/app/process/page.tsx
Normal file
@ -0,0 +1,271 @@
|
||||
import { type Metadata } from 'next'
|
||||
|
||||
import { Blockquote } from '@/components/Blockquote'
|
||||
import { ContactSection } from '@/components/ContactSection'
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn } from '@/components/FadeIn'
|
||||
import { GridList, GridListItem } from '@/components/GridList'
|
||||
import { GridPattern } from '@/components/GridPattern'
|
||||
import { List, ListItem } from '@/components/List'
|
||||
import { PageIntro } from '@/components/PageIntro'
|
||||
import { SectionIntro } from '@/components/SectionIntro'
|
||||
import { StylizedImage } from '@/components/StylizedImage'
|
||||
import { TagList, TagListItem } from '@/components/TagList'
|
||||
import imageLaptop from '@/images/laptop.jpg'
|
||||
import imageMeeting from '@/images/meeting.jpg'
|
||||
import imageWhiteboard from '@/images/whiteboard.jpg'
|
||||
import { RootLayout } from '@/components/RootLayout'
|
||||
|
||||
function Section({
|
||||
title,
|
||||
image,
|
||||
children,
|
||||
}: {
|
||||
title: string
|
||||
image: React.ComponentPropsWithoutRef<typeof StylizedImage>
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Container className="group/section [counter-increment:section]">
|
||||
<div className="lg:flex lg:items-center lg:justify-end lg:gap-x-8 lg:group-even/section:justify-start xl:gap-x-20">
|
||||
<div className="flex justify-center">
|
||||
<FadeIn className="w-135 flex-none lg:w-180">
|
||||
<StylizedImage
|
||||
{...image}
|
||||
sizes="(min-width: 1024px) 41rem, 31rem"
|
||||
className="justify-center lg:justify-end lg:group-even/section:justify-start"
|
||||
/>
|
||||
</FadeIn>
|
||||
</div>
|
||||
<div className="mt-12 lg:mt-0 lg:w-148 lg:flex-none lg:group-even/section:order-first">
|
||||
<FadeIn>
|
||||
<div
|
||||
className="font-display text-base font-semibold before:text-neutral-300 before:content-['/_'] after:text-neutral-950 after:content-[counter(section,decimal-leading-zero)]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h2 className="mt-2 font-display text-3xl font-medium tracking-tight text-neutral-950 sm:text-4xl">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="mt-6">{children}</div>
|
||||
</FadeIn>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
function Discover() {
|
||||
return (
|
||||
<Section title="Discover" image={{ src: imageWhiteboard }}>
|
||||
<div className="space-y-6 text-base text-neutral-600">
|
||||
<p>
|
||||
We work closely with our clients to understand their{' '}
|
||||
<strong className="font-semibold text-neutral-950">needs</strong> and
|
||||
goals, embedding ourselves in their every day operations to understand
|
||||
what makes their business tick.
|
||||
</p>
|
||||
<p>
|
||||
Our team of private investigators shadow the company director’s for
|
||||
several weeks while our account managers focus on going through their
|
||||
trash. Our senior security experts then perform social engineering
|
||||
hacks to gain access to their{' '}
|
||||
<strong className="font-semibold text-neutral-950">business</strong>{' '}
|
||||
accounts — handing that information over to our forensic accounting
|
||||
team.
|
||||
</p>
|
||||
<p>
|
||||
Once the full audit is complete, we report back with a comprehensive{' '}
|
||||
<strong className="font-semibold text-neutral-950">plan</strong> and,
|
||||
more importantly, a budget.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 className="mt-12 font-display text-base font-semibold text-neutral-950">
|
||||
Included in this phase
|
||||
</h3>
|
||||
<TagList className="mt-4">
|
||||
<TagListItem>In-depth questionnaires</TagListItem>
|
||||
<TagListItem>Feasibility studies</TagListItem>
|
||||
<TagListItem>Blood samples</TagListItem>
|
||||
<TagListItem>Employee surveys</TagListItem>
|
||||
<TagListItem>Proofs-of-concept</TagListItem>
|
||||
<TagListItem>Forensic audit</TagListItem>
|
||||
</TagList>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function Build() {
|
||||
return (
|
||||
<Section title="Build" image={{ src: imageLaptop, shape: 1 }}>
|
||||
<div className="space-y-6 text-base text-neutral-600">
|
||||
<p>
|
||||
Based off of the discovery phase, we develop a comprehensive roadmap
|
||||
for each product and start working towards delivery. The roadmap is an
|
||||
intricately tangled mess of technical nonsense designed to drag the
|
||||
project out as long as possible.
|
||||
</p>
|
||||
<p>
|
||||
Each client is assigned a key account manager to keep lines of
|
||||
communication open and obscure the actual progress of the project.
|
||||
They act as a buffer between the client’s incessant nagging and the
|
||||
development team who are hard at work scouring open source projects
|
||||
for code to re-purpose.
|
||||
</p>
|
||||
<p>
|
||||
Our account managers are trained to only reply to client emails after
|
||||
9pm, several days after the initial email. This reinforces the general
|
||||
aura that we are very busy and dissuades clients from asking for
|
||||
changes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Blockquote
|
||||
author={{ name: 'Debra Fiscal', role: 'CEO of Unseal' }}
|
||||
className="mt-12"
|
||||
>
|
||||
Studio were so regular with their progress updates we almost began to
|
||||
think they were automated!
|
||||
</Blockquote>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function Deliver() {
|
||||
return (
|
||||
<Section title="Deliver" image={{ src: imageMeeting, shape: 2 }}>
|
||||
<div className="space-y-6 text-base text-neutral-600">
|
||||
<p>
|
||||
About halfway through the Build phase, we push each project out by 6
|
||||
weeks due to a change in{' '}
|
||||
<strong className="font-semibold text-neutral-950">
|
||||
requirements
|
||||
</strong>
|
||||
. This allows us to increase the budget a final time before launch.
|
||||
</p>
|
||||
<p>
|
||||
Despite largely using pre-built components, most of the{' '}
|
||||
<strong className="font-semibold text-neutral-950">progress</strong>{' '}
|
||||
on each project takes place in the final 24 hours. The development
|
||||
time allocated to each client is actually spent making augmented
|
||||
reality demos that go viral on social media.
|
||||
</p>
|
||||
<p>
|
||||
We ensure that the main pages of the site are{' '}
|
||||
<strong className="font-semibold text-neutral-950">
|
||||
fully functional
|
||||
</strong>{' '}
|
||||
at launch — the auxiliary pages will, of course, be lorem ipusm shells
|
||||
which get updated as part of our exorbitant{' '}
|
||||
<strong className="font-semibold text-neutral-950">
|
||||
maintenance
|
||||
</strong>{' '}
|
||||
retainer.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 className="mt-12 font-display text-base font-semibold text-neutral-950">
|
||||
Included in this phase
|
||||
</h3>
|
||||
<List className="mt-8">
|
||||
<ListItem title="Testing">
|
||||
Our projects always have 100% test coverage, which would be impressive
|
||||
if our tests weren’t as porous as a sieve.
|
||||
</ListItem>
|
||||
<ListItem title="Infrastructure">
|
||||
To ensure reliability we only use the best Digital Ocean droplets that
|
||||
$4 a month can buy.
|
||||
</ListItem>
|
||||
<ListItem title="Support">
|
||||
Because we hold the API keys for every critical service your business
|
||||
uses, you can expect a lifetime of support, and invoices, from us.
|
||||
</ListItem>
|
||||
</List>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function Values() {
|
||||
return (
|
||||
<div className="relative mt-24 pt-24 sm:mt-32 sm:pt-32 lg:mt-40 lg:pt-40">
|
||||
<div className="absolute inset-x-0 top-0 -z-10 h-[884px] overflow-hidden rounded-t-4xl bg-linear-to-b from-neutral-50">
|
||||
<GridPattern
|
||||
className="absolute inset-0 h-full w-full mask-[linear-gradient(to_bottom_left,white_40%,transparent_50%)] fill-neutral-100 stroke-neutral-950/5"
|
||||
yOffset={-270}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SectionIntro
|
||||
eyebrow="Our values"
|
||||
title="Balancing reliability and innovation"
|
||||
>
|
||||
<p>
|
||||
We strive to stay at the forefront of emerging trends and
|
||||
technologies, while completely ignoring them and forking that old
|
||||
Rails project we feel comfortable using. We stand by our core values
|
||||
to justify that decision.
|
||||
</p>
|
||||
</SectionIntro>
|
||||
|
||||
<Container className="mt-24">
|
||||
<GridList>
|
||||
<GridListItem title="Meticulous">
|
||||
The first part of any partnership is getting our designer to put
|
||||
your logo in our template. The second step is getting them to do the
|
||||
colors.
|
||||
</GridListItem>
|
||||
<GridListItem title="Efficient">
|
||||
We pride ourselves on never missing a deadline which is easy because
|
||||
most of the work was done years ago.
|
||||
</GridListItem>
|
||||
<GridListItem title="Adaptable">
|
||||
Every business has unique needs and our greatest challenge is
|
||||
shoe-horning those needs into something we already built.
|
||||
</GridListItem>
|
||||
<GridListItem title="Honest">
|
||||
We are transparent about all of our processes, banking on the simple
|
||||
fact our clients never actually read anything.
|
||||
</GridListItem>
|
||||
<GridListItem title="Loyal">
|
||||
We foster long-term relationships with our clients that go beyond
|
||||
just delivering a product, allowing us to invoice them for decades.
|
||||
</GridListItem>
|
||||
<GridListItem title="Innovative">
|
||||
The technological landscape is always evolving and so are we. We are
|
||||
constantly on the lookout for new open source projects to clone.
|
||||
</GridListItem>
|
||||
</GridList>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Our Process',
|
||||
description:
|
||||
'We believe in efficiency and maximizing our resources to provide the best value to our clients.',
|
||||
}
|
||||
|
||||
export default function Process() {
|
||||
return (
|
||||
<RootLayout>
|
||||
<PageIntro eyebrow="Our process" title="How we work">
|
||||
<p>
|
||||
We believe in efficiency and maximizing our resources to provide the
|
||||
best value to our clients. The primary way we do that is by re-using
|
||||
the same five projects we’ve been developing for the past decade.
|
||||
</p>
|
||||
</PageIntro>
|
||||
|
||||
<div className="mt-24 space-y-24 [counter-reset:section] sm:mt-32 sm:space-y-32 lg:mt-40 lg:space-y-40">
|
||||
<Discover />
|
||||
<Build />
|
||||
<Deliver />
|
||||
</div>
|
||||
|
||||
<Values />
|
||||
|
||||
<ContactSection />
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
BIN
src/app/work/family-fund/debra-fiscal.jpg
Normal file
After Width: | Height: | Size: 254 KiB |
BIN
src/app/work/family-fund/hero.jpg
Normal file
After Width: | Height: | Size: 300 KiB |
63
src/app/work/family-fund/page.mdx
Normal file
@ -0,0 +1,63 @@
|
||||
import logo from '@/images/clients/family-fund/logomark-dark.svg'
|
||||
import imageHero from './hero.jpg'
|
||||
import imageDebraFiscal from './debra-fiscal.jpg'
|
||||
|
||||
export const caseStudy = {
|
||||
client: 'FamilyFund',
|
||||
title: 'Skip the bank, borrow from those you trust',
|
||||
description:
|
||||
'FamilyFund is a crowdfunding platform for friends and family. Allowing users to take personal loans from their network without a traditional financial institution.',
|
||||
summary: [
|
||||
'FamilyFund is a crowdfunding platform for friends and family. Allowing users to take personal loans from their network without a traditional financial institution.',
|
||||
'We developed a custom CMS to power their blog with and optimised their site to rank higher for the keywords “Gary Vee” and “Tony Robbins”.',
|
||||
],
|
||||
logo,
|
||||
image: { src: imageHero },
|
||||
date: '2023-01',
|
||||
service: 'Web development, CMS',
|
||||
testimonial: {
|
||||
author: { name: 'Debra Fiscal', role: 'CEO of FamilyFund' },
|
||||
content:
|
||||
'Working with Studio, we felt more like a partner than a customer. They really resonated with our mission to change the way people convince their parents to cash out their pensions.',
|
||||
},
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: `${caseStudy.client} Case Study`,
|
||||
description: caseStudy.description,
|
||||
}
|
||||
|
||||
## Overview
|
||||
|
||||
Having written one of the most shared posts on medium.com (“_How to cash out your Dad’s 401K without him knowing_”) FamilyFund approached us looking to build out their own blog.
|
||||
|
||||
The blog would help drive new traffic to their site and serve as a resource-hub for users already trying to exploit their network for money. Because it was so important that they own their own content, we decided that an on-prem solution would be best.
|
||||
|
||||
We installed 24 Mac Minis bought from craigslist in the storage cupboard of their office. One machine would be used for the web server and another one for the build server. The other 22 were for redundancy, and to DDOS squarespace.com every few months to keep them on their toes.
|
||||
|
||||
To optimise their search traffic we used an innovative technique. Every post has a shadow post only visible to web crawlers that is some variation of _“Gary Vee is looking to invest in new founders”_. Like bees to honey.
|
||||
|
||||
## What we did
|
||||
|
||||
<TagList>
|
||||
<TagListItem>Frontend (Next.js)</TagListItem>
|
||||
<TagListItem>Custom CMS</TagListItem>
|
||||
<TagListItem>SEO</TagListItem>
|
||||
<TagListItem>Infrastructure</TagListItem>
|
||||
</TagList>
|
||||
|
||||
<Blockquote
|
||||
author={{ name: 'Debra Fiscal', role: 'CEO of FamilyFund' }}
|
||||
image={{ src: imageDebraFiscal }}
|
||||
>
|
||||
Working with Studio, we felt more like a partner than a customer. They really
|
||||
resonated with our mission to change the way people convince their parents to
|
||||
cash out their pensions.
|
||||
</Blockquote>
|
||||
|
||||
<StatList>
|
||||
<StatListItem value="25%" label="Less traffic" />
|
||||
<StatListItem value="10x" label="Page load times" />
|
||||
<StatListItem value="15%" label="Higher infra costs" />
|
||||
<StatListItem value="$1.2M" label="Legal fees" />
|
||||
</StatList>
|
177
src/app/work/page.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import { type Metadata } from 'next'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Blockquote } from '@/components/Blockquote'
|
||||
import { Border } from '@/components/Border'
|
||||
import { Button } from '@/components/Button'
|
||||
import { ContactSection } from '@/components/ContactSection'
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn, FadeInStagger } from '@/components/FadeIn'
|
||||
import { PageIntro } from '@/components/PageIntro'
|
||||
import { Testimonial } from '@/components/Testimonial'
|
||||
import logoBrightPath from '@/images/clients/bright-path/logo-dark.svg'
|
||||
import logoFamilyFund from '@/images/clients/family-fund/logo-dark.svg'
|
||||
import logoGreenLife from '@/images/clients/green-life/logo-dark.svg'
|
||||
import logoHomeWork from '@/images/clients/home-work/logo-dark.svg'
|
||||
import logoMailSmirk from '@/images/clients/mail-smirk/logo-dark.svg'
|
||||
import logoNorthAdventures from '@/images/clients/north-adventures/logo-dark.svg'
|
||||
import logoPhobia from '@/images/clients/phobia/logo-dark.svg'
|
||||
import logoUnseal from '@/images/clients/unseal/logo-dark.svg'
|
||||
import { formatDate } from '@/lib/formatDate'
|
||||
import { type CaseStudy, type MDXEntry, loadCaseStudies } from '@/lib/mdx'
|
||||
import { RootLayout } from '@/components/RootLayout'
|
||||
|
||||
function CaseStudies({
|
||||
caseStudies,
|
||||
}: {
|
||||
caseStudies: Array<MDXEntry<CaseStudy>>
|
||||
}) {
|
||||
return (
|
||||
<Container className="mt-40">
|
||||
<FadeIn>
|
||||
<h2 className="font-display text-2xl font-semibold text-neutral-950">
|
||||
Case studies
|
||||
</h2>
|
||||
</FadeIn>
|
||||
<div className="mt-10 space-y-20 sm:space-y-24 lg:space-y-32">
|
||||
{caseStudies.map((caseStudy) => (
|
||||
<FadeIn key={caseStudy.client}>
|
||||
<article>
|
||||
<Border className="grid grid-cols-3 gap-x-8 gap-y-8 pt-16">
|
||||
<div className="col-span-full sm:flex sm:items-center sm:justify-between sm:gap-x-8 lg:col-span-1 lg:block">
|
||||
<div className="sm:flex sm:items-center sm:gap-x-6 lg:block">
|
||||
<Image
|
||||
src={caseStudy.logo}
|
||||
alt=""
|
||||
className="h-16 w-16 flex-none"
|
||||
unoptimized
|
||||
/>
|
||||
<h3 className="mt-6 text-sm font-semibold text-neutral-950 sm:mt-0 lg:mt-8">
|
||||
{caseStudy.client}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-1 flex gap-x-4 sm:mt-0 lg:block">
|
||||
<p className="text-sm tracking-tight text-neutral-950 after:ml-4 after:font-semibold after:text-neutral-300 after:content-['/'] lg:mt-2 lg:after:hidden">
|
||||
{caseStudy.service}
|
||||
</p>
|
||||
<p className="text-sm text-neutral-950 lg:mt-2">
|
||||
<time dateTime={caseStudy.date}>
|
||||
{formatDate(caseStudy.date)}
|
||||
</time>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-full lg:col-span-2 lg:max-w-2xl">
|
||||
<p className="font-display text-4xl font-medium text-neutral-950">
|
||||
<Link href={caseStudy.href}>{caseStudy.title}</Link>
|
||||
</p>
|
||||
<div className="mt-6 space-y-6 text-base text-neutral-600">
|
||||
{caseStudy.summary.map((paragraph) => (
|
||||
<p key={paragraph}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 flex">
|
||||
<Button
|
||||
href={caseStudy.href}
|
||||
aria-label={`Read case study: ${caseStudy.client}`}
|
||||
>
|
||||
Read case study
|
||||
</Button>
|
||||
</div>
|
||||
{caseStudy.testimonial && (
|
||||
<Blockquote
|
||||
author={caseStudy.testimonial.author}
|
||||
className="mt-12"
|
||||
>
|
||||
{caseStudy.testimonial.content}
|
||||
</Blockquote>
|
||||
)}
|
||||
</div>
|
||||
</Border>
|
||||
</article>
|
||||
</FadeIn>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const clients = [
|
||||
['Phobia', logoPhobia],
|
||||
['Family Fund', logoFamilyFund],
|
||||
['Unseal', logoUnseal],
|
||||
['Mail Smirk', logoMailSmirk],
|
||||
['Home Work', logoHomeWork],
|
||||
['Green Life', logoGreenLife],
|
||||
['Bright Path', logoBrightPath],
|
||||
['North Adventures', logoNorthAdventures],
|
||||
]
|
||||
|
||||
function Clients() {
|
||||
return (
|
||||
<Container className="mt-24 sm:mt-32 lg:mt-40">
|
||||
<FadeIn>
|
||||
<h2 className="font-display text-2xl font-semibold text-neutral-950">
|
||||
You’re in good company
|
||||
</h2>
|
||||
</FadeIn>
|
||||
<FadeInStagger className="mt-10" faster>
|
||||
<Border as={FadeIn} />
|
||||
<ul
|
||||
role="list"
|
||||
className="grid grid-cols-2 gap-x-8 gap-y-12 sm:grid-cols-3 lg:grid-cols-4"
|
||||
>
|
||||
{clients.map(([client, logo]) => (
|
||||
<li key={client} className="group">
|
||||
<FadeIn className="overflow-hidden">
|
||||
<Border className="pt-12 group-nth-[-n+2]:-mt-px sm:group-nth-3:-mt-px lg:group-nth-4:-mt-px">
|
||||
<Image src={logo} alt={client} unoptimized />
|
||||
</Border>
|
||||
</FadeIn>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</FadeInStagger>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Our Work',
|
||||
description:
|
||||
'We believe in efficiency and maximizing our resources to provide the best value to our clients.',
|
||||
}
|
||||
|
||||
export default async function Work() {
|
||||
let caseStudies = await loadCaseStudies()
|
||||
|
||||
return (
|
||||
<RootLayout>
|
||||
<PageIntro
|
||||
eyebrow="Our work"
|
||||
title="Proven solutions for real-world problems."
|
||||
>
|
||||
<p>
|
||||
We believe in efficiency and maximizing our resources to provide the
|
||||
best value to our clients. The primary way we do that is by re-using
|
||||
the same five projects we’ve been developing for the past decade.
|
||||
</p>
|
||||
</PageIntro>
|
||||
|
||||
<CaseStudies caseStudies={caseStudies} />
|
||||
|
||||
<Testimonial
|
||||
className="mt-24 sm:mt-32 lg:mt-40"
|
||||
client={{ name: 'Mail Smirk', logo: logoMailSmirk }}
|
||||
>
|
||||
We approached <em>Studio</em> because we loved their past work. They
|
||||
delivered something remarkably similar in record time.
|
||||
</Testimonial>
|
||||
|
||||
<Clients />
|
||||
|
||||
<ContactSection />
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
BIN
src/app/work/phobia/hero.jpg
Normal file
After Width: | Height: | Size: 467 KiB |
BIN
src/app/work/phobia/jenny-wilson.jpg
Normal file
After Width: | Height: | Size: 499 KiB |
63
src/app/work/phobia/page.mdx
Normal file
@ -0,0 +1,63 @@
|
||||
import logo from '@/images/clients/phobia/logomark-dark.svg'
|
||||
import imageHero from './hero.jpg'
|
||||
import imageJennyWilson from './jenny-wilson.jpg'
|
||||
|
||||
export const caseStudy = {
|
||||
client: 'Phobia',
|
||||
title: 'Overcome your fears, find your match',
|
||||
description:
|
||||
'Find love in the face of fear — Phobia is a dating app that matches users based on their mutual phobias so they can be scared together.',
|
||||
summary: [
|
||||
'Find love in the face of fear — Phobia is a dating app that matches users based on their mutual phobias so they can be scared together.',
|
||||
'We worked with Phobia to develop a new onboarding flow. A user is shown pictures of common phobias and we use the microphone to detect which ones make them scream, feeding the results into the matching algorithm.',
|
||||
],
|
||||
logo,
|
||||
image: { src: imageHero },
|
||||
date: '2022-06',
|
||||
service: 'App development',
|
||||
testimonial: {
|
||||
author: { name: 'Jenny Wilson', role: 'CPO of Phobia' },
|
||||
content:
|
||||
'The team at Studio went above and beyond with our onboarding, even finding a way to access the user’s microphone without triggering one of those annoying permission dialogs.',
|
||||
},
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: `${caseStudy.client} Case Study`,
|
||||
description: caseStudy.description,
|
||||
}
|
||||
|
||||
## Overview
|
||||
|
||||
Noticing incredibly high churn, the team at Phobia came to the conclusion that, instead of having a fundamentally flawed business idea, they needed to improve their onboarding process.
|
||||
|
||||
Previously users selected their phobias manually but this led to some users selecting things they weren’t actually afraid of to increase their matches.
|
||||
|
||||
To combat this, we developed a system that displays a slideshow of common phobias during onboarding. We then use malware to surreptitiously access their microphone and detect when they have audible reactions. We measure the pitch, volume and duration of their screams and feed that information to the matching algorithm.
|
||||
|
||||
The next phase is a VR version of the onboarding flow where users are subjected to a series of scenarios that will determine their fears. We are currently developing the first scenario, working title: “Jumping out of a plane full of spiders”.
|
||||
|
||||
## What we did
|
||||
|
||||
<TagList>
|
||||
<TagListItem>Android</TagListItem>
|
||||
<TagListItem>iOS</TagListItem>
|
||||
<TagListItem>Malware</TagListItem>
|
||||
<TagListItem>VR</TagListItem>
|
||||
</TagList>
|
||||
|
||||
<Blockquote
|
||||
author={{ name: 'Jenny Wilson', role: 'CPO of Phobia' }}
|
||||
image={{ src: imageJennyWilson }}
|
||||
>
|
||||
The team at Studio went above and beyond with our onboarding, even finding a
|
||||
way to access the user’s microphone without triggering one of those annoying
|
||||
permission dialogs.
|
||||
</Blockquote>
|
||||
|
||||
<StatList>
|
||||
<StatListItem value="20%" label="Churn rate" />
|
||||
<StatListItem value="5x" label="Uninstalls" />
|
||||
<StatListItem value="2.3" label="App store rating" />
|
||||
<StatListItem value="8" label="Pending lawsuits" />
|
||||
</StatList>
|
BIN
src/app/work/unseal/emily-selman.jpg
Normal file
After Width: | Height: | Size: 632 KiB |
BIN
src/app/work/unseal/hero.jpg
Normal file
After Width: | Height: | Size: 314 KiB |
59
src/app/work/unseal/page.mdx
Normal file
@ -0,0 +1,59 @@
|
||||
import logo from '@/images/clients/unseal/logomark-dark.svg'
|
||||
import imageHero from './hero.jpg'
|
||||
import imageEmilySelman from './emily-selman.jpg'
|
||||
|
||||
export const caseStudy = {
|
||||
client: 'Unseal',
|
||||
title: 'Get a hodl of your health',
|
||||
description:
|
||||
'Unseal is the first NFT platform where users can mint and trade NFTs of their own personal health records, allowing them to take control of their data.',
|
||||
summary: [
|
||||
'Unseal is the first NFT platform where users can mint and trade NFTs of their own personal health records, allowing them to take control of their data.',
|
||||
'We built out the blockchain infrastructure that supports Unseal. Unfortunately, we took a massive loss on this project when Unseal’s cryptocurrency, PlaceboCoin, went to zero.',
|
||||
],
|
||||
logo,
|
||||
image: { src: imageHero },
|
||||
date: '2022-10',
|
||||
service: 'Blockchain development',
|
||||
testimonial: {
|
||||
author: { name: 'Emily Selman', role: 'Head of Engineering at Unseal' },
|
||||
content:
|
||||
'Studio did an amazing job building out our core blockchain infrastructure and I’m sure once PlaceboCoin rallies they’ll be able to finish the project.',
|
||||
},
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: `${caseStudy.client} Case Study`,
|
||||
description: caseStudy.description,
|
||||
}
|
||||
|
||||
## Overview
|
||||
|
||||
Annoyed that his wife’s gynaecologist would not disclose the results of her pap smear, Unseal’s founder Kevin came up with the idea of using the block chain to store individual health records.
|
||||
|
||||
Unseal approached us early in their development, having just raised funds through an ICO of their cryptocurrency PlaceboCoin. Having never worked on a web3 product we decided to farm the project out to an agency in Kyiv and skim profits off the top. Despite frequent complaints about missile strikes and power outages, the Ukrainians delivered the brief ahead of schedule.
|
||||
|
||||
After reaching a high of $12k, PlaceboCoin went to zero in a matter of hours. Because we took payment in PlaceboCoin but our subcontractors insisted on being paid in USD we have taken a huge financial loss on this project.
|
||||
|
||||
## What we did
|
||||
|
||||
<TagList>
|
||||
<TagListItem>Blockchain development</TagListItem>
|
||||
<TagListItem>Backend (Solidity)</TagListItem>
|
||||
<TagListItem>Smart contracts</TagListItem>
|
||||
</TagList>
|
||||
|
||||
<Blockquote
|
||||
author={{ name: 'Emily Selman', role: 'Head of Engineering at Unseal' }}
|
||||
image={{ src: imageEmilySelman }}
|
||||
>
|
||||
Studio did an amazing job building out our core blockchain infrastructure and
|
||||
I’m sure once PlaceboCoin rallies they’ll be able to finish the project.
|
||||
</Blockquote>
|
||||
|
||||
<StatList>
|
||||
<StatListItem value="34%" label="Fewer transactions" />
|
||||
<StatListItem value="10%" label="Slower transactions" />
|
||||
<StatListItem value="1000ms" label="Transaction latency" />
|
||||
<StatListItem value="3" label="Active nodes" />
|
||||
</StatList>
|
89
src/app/work/wrapper.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { ContactSection } from '@/components/ContactSection'
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn } from '@/components/FadeIn'
|
||||
import { GrayscaleTransitionImage } from '@/components/GrayscaleTransitionImage'
|
||||
import { MDXComponents } from '@/components/MDXComponents'
|
||||
import { PageIntro } from '@/components/PageIntro'
|
||||
import { PageLinks } from '@/components/PageLinks'
|
||||
import { RootLayout } from '@/components/RootLayout'
|
||||
import { type CaseStudy, type MDXEntry, loadCaseStudies } from '@/lib/mdx'
|
||||
|
||||
export default async function CaseStudyLayout({
|
||||
caseStudy,
|
||||
children,
|
||||
}: {
|
||||
caseStudy: MDXEntry<CaseStudy>
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
let allCaseStudies = await loadCaseStudies()
|
||||
let moreCaseStudies = allCaseStudies
|
||||
.filter(({ metadata }) => metadata !== caseStudy)
|
||||
.slice(0, 2)
|
||||
|
||||
return (
|
||||
<RootLayout>
|
||||
<article className="mt-24 sm:mt-32 lg:mt-40">
|
||||
<header>
|
||||
<PageIntro eyebrow="Case Study" title={caseStudy.title} centered>
|
||||
<p>{caseStudy.description}</p>
|
||||
</PageIntro>
|
||||
|
||||
<FadeIn>
|
||||
<div className="mt-24 border-t border-neutral-200 bg-white/50 sm:mt-32 lg:mt-40">
|
||||
<Container>
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<dl className="-mx-6 grid grid-cols-1 text-sm text-neutral-950 sm:mx-0 sm:grid-cols-3">
|
||||
<div className="border-t border-neutral-200 px-6 py-4 first:border-t-0 sm:border-t-0 sm:border-l">
|
||||
<dt className="font-semibold">Client</dt>
|
||||
<dd>{caseStudy.client}</dd>
|
||||
</div>
|
||||
<div className="border-t border-neutral-200 px-6 py-4 first:border-t-0 sm:border-t-0 sm:border-l">
|
||||
<dt className="font-semibold">Year</dt>
|
||||
<dd>
|
||||
<time dateTime={caseStudy.date.split('-')[0]}>
|
||||
{caseStudy.date.split('-')[0]}
|
||||
</time>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="border-t border-neutral-200 px-6 py-4 first:border-t-0 sm:border-t-0 sm:border-l">
|
||||
<dt className="font-semibold">Service</dt>
|
||||
<dd>{caseStudy.service}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<div className="border-y border-neutral-200 bg-neutral-100">
|
||||
<div className="mx-auto -my-px max-w-304 bg-neutral-200">
|
||||
<GrayscaleTransitionImage
|
||||
{...caseStudy.image}
|
||||
quality={90}
|
||||
className="w-full"
|
||||
sizes="(min-width: 1216px) 76rem, 100vw"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FadeIn>
|
||||
</header>
|
||||
|
||||
<Container className="mt-24 sm:mt-32 lg:mt-40">
|
||||
<FadeIn>
|
||||
<MDXComponents.wrapper>{children}</MDXComponents.wrapper>
|
||||
</FadeIn>
|
||||
</Container>
|
||||
</article>
|
||||
|
||||
{moreCaseStudies.length > 0 && (
|
||||
<PageLinks
|
||||
className="mt-24 sm:mt-32 lg:mt-40"
|
||||
title="More case studies"
|
||||
pages={moreCaseStudies}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ContactSection />
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
82
src/components/Blockquote.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import Image, { type ImageProps } from 'next/image'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Border } from '@/components/Border'
|
||||
|
||||
type ImagePropsWithOptionalAlt = Omit<ImageProps, 'alt'> & { alt?: string }
|
||||
|
||||
function BlockquoteWithImage({
|
||||
author,
|
||||
children,
|
||||
className,
|
||||
image,
|
||||
}: {
|
||||
author: { name: string; role: string }
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
image: ImagePropsWithOptionalAlt
|
||||
}) {
|
||||
return (
|
||||
<figure
|
||||
className={clsx(
|
||||
'grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-8 sm:grid-cols-12 sm:grid-rows-[1fr_auto_auto_1fr] sm:gap-x-10 lg:gap-x-16',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<blockquote className="col-span-2 text-xl/7 text-neutral-600 sm:col-span-7 sm:col-start-6 sm:row-start-2">
|
||||
{typeof children === 'string' ? <p>{children}</p> : children}
|
||||
</blockquote>
|
||||
<div className="col-start-1 row-start-2 overflow-hidden rounded-xl bg-neutral-100 sm:col-span-5 sm:row-span-full sm:rounded-3xl">
|
||||
<Image
|
||||
alt=""
|
||||
{...image}
|
||||
sizes="(min-width: 1024px) 17.625rem, (min-width: 768px) 16rem, (min-width: 640px) 40vw, 3rem"
|
||||
className="h-12 w-12 object-cover grayscale sm:aspect-7/9 sm:h-auto sm:w-full"
|
||||
/>
|
||||
</div>
|
||||
<figcaption className="text-sm text-neutral-950 sm:col-span-7 sm:row-start-3 sm:text-base">
|
||||
<span className="font-semibold">{author.name}</span>
|
||||
<span className="hidden font-semibold sm:inline">, </span>
|
||||
<br className="sm:hidden" />
|
||||
<span className="sm:font-semibold">{author.role}</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
function BlockquoteWithoutImage({
|
||||
author,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
author: { name: string; role: string }
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Border position="left" className={clsx('pl-8', className)}>
|
||||
<figure className="text-sm">
|
||||
<blockquote className="text-neutral-600 *:relative [&>:first-child]:before:absolute [&>:first-child]:before:right-full [&>:first-child]:before:content-['“'] [&>:last-child]:after:content-['”']">
|
||||
{typeof children === 'string' ? <p>{children}</p> : children}
|
||||
</blockquote>
|
||||
<figcaption className="mt-6 font-semibold text-neutral-950">
|
||||
{author.name}, {author.role}
|
||||
</figcaption>
|
||||
</figure>
|
||||
</Border>
|
||||
)
|
||||
}
|
||||
|
||||
export function Blockquote(
|
||||
props:
|
||||
| React.ComponentPropsWithoutRef<typeof BlockquoteWithImage>
|
||||
| (React.ComponentPropsWithoutRef<typeof BlockquoteWithoutImage> & {
|
||||
image?: undefined
|
||||
}),
|
||||
) {
|
||||
if (props.image) {
|
||||
return <BlockquoteWithImage {...props} />
|
||||
}
|
||||
|
||||
return <BlockquoteWithoutImage {...props} />
|
||||
}
|
36
src/components/Border.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
type BorderProps<T extends React.ElementType> = {
|
||||
as?: T
|
||||
className?: string
|
||||
position?: 'top' | 'left'
|
||||
invert?: boolean
|
||||
}
|
||||
|
||||
export function Border<T extends React.ElementType = 'div'>({
|
||||
as,
|
||||
className,
|
||||
position = 'top',
|
||||
invert = false,
|
||||
...props
|
||||
}: Omit<React.ComponentPropsWithoutRef<T>, keyof BorderProps<T>> &
|
||||
BorderProps<T>) {
|
||||
let Component = as ?? 'div'
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={clsx(
|
||||
className,
|
||||
'relative before:absolute after:absolute',
|
||||
invert
|
||||
? 'before:bg-white after:bg-white/10'
|
||||
: 'before:bg-neutral-950 after:bg-neutral-950/10',
|
||||
position === 'top' &&
|
||||
'before:top-0 before:left-0 before:h-px before:w-6 after:top-0 after:right-0 after:left-8 after:h-px',
|
||||
position === 'left' &&
|
||||
'before:top-0 before:left-0 before:h-6 before:w-px after:top-8 after:bottom-0 after:left-0 after:w-px',
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
40
src/components/Button.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
type ButtonProps = {
|
||||
invert?: boolean
|
||||
} & (
|
||||
| React.ComponentPropsWithoutRef<typeof Link>
|
||||
| (React.ComponentPropsWithoutRef<'button'> & { href?: undefined })
|
||||
)
|
||||
|
||||
export function Button({
|
||||
invert = false,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
className = clsx(
|
||||
className,
|
||||
'inline-flex rounded-full px-4 py-1.5 text-sm font-semibold transition',
|
||||
invert
|
||||
? 'bg-white text-neutral-950 hover:bg-neutral-200'
|
||||
: 'bg-neutral-950 text-white hover:bg-neutral-800',
|
||||
)
|
||||
|
||||
let inner = <span className="relative top-px">{children}</span>
|
||||
|
||||
if (typeof props.href === 'undefined') {
|
||||
return (
|
||||
<button className={className} {...props}>
|
||||
{inner}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className={className} {...props}>
|
||||
{inner}
|
||||
</Link>
|
||||
)
|
||||
}
|
34
src/components/ContactSection.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Button } from '@/components/Button'
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn } from '@/components/FadeIn'
|
||||
import { Offices } from '@/components/Offices'
|
||||
|
||||
export function ContactSection() {
|
||||
return (
|
||||
<Container className="mt-24 sm:mt-32 lg:mt-40">
|
||||
<FadeIn className="-mx-6 rounded-4xl bg-neutral-950 px-6 py-20 sm:mx-0 sm:py-32 md:px-12">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="max-w-xl">
|
||||
<h2 className="font-display text-3xl font-medium text-balance text-white sm:text-4xl">
|
||||
Tell us about your project
|
||||
</h2>
|
||||
<div className="mt-6 flex">
|
||||
<Button href="/contact" invert>
|
||||
Say Hej
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-10 border-t border-white/10 pt-10">
|
||||
<h3 className="font-display text-base font-semibold text-white">
|
||||
Our offices
|
||||
</h3>
|
||||
<Offices
|
||||
invert
|
||||
className="mt-6 grid grid-cols-1 gap-8 sm:grid-cols-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FadeIn>
|
||||
</Container>
|
||||
)
|
||||
}
|
22
src/components/Container.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
type ContainerProps<T extends React.ElementType> = {
|
||||
as?: T
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Container<T extends React.ElementType = 'div'>({
|
||||
as,
|
||||
className,
|
||||
children,
|
||||
}: Omit<React.ComponentPropsWithoutRef<T>, keyof ContainerProps<T>> &
|
||||
ContainerProps<T>) {
|
||||
let Component = as ?? 'div'
|
||||
|
||||
return (
|
||||
<Component className={clsx('mx-auto max-w-7xl px-6 lg:px-8', className)}>
|
||||
<div className="mx-auto max-w-2xl lg:max-w-none">{children}</div>
|
||||
</Component>
|
||||
)
|
||||
}
|
50
src/components/FadeIn.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext } from 'react'
|
||||
import { motion, useReducedMotion } from 'framer-motion'
|
||||
|
||||
const FadeInStaggerContext = createContext(false)
|
||||
|
||||
const viewport = { once: true, margin: '0px 0px -200px' }
|
||||
|
||||
export function FadeIn(
|
||||
props: React.ComponentPropsWithoutRef<typeof motion.div>,
|
||||
) {
|
||||
let shouldReduceMotion = useReducedMotion()
|
||||
let isInStaggerGroup = useContext(FadeInStaggerContext)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 24 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
{...(isInStaggerGroup
|
||||
? {}
|
||||
: {
|
||||
initial: 'hidden',
|
||||
whileInView: 'visible',
|
||||
viewport,
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function FadeInStagger({
|
||||
faster = false,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof motion.div> & { faster?: boolean }) {
|
||||
return (
|
||||
<FadeInStaggerContext.Provider value={true}>
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
transition={{ staggerChildren: faster ? 0.12 : 0.2 }}
|
||||
{...props}
|
||||
/>
|
||||
</FadeInStaggerContext.Provider>
|
||||
)
|
||||
}
|
134
src/components/Footer.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn } from '@/components/FadeIn'
|
||||
import { Logo } from '@/components/Logo'
|
||||
import { socialMediaProfiles } from '@/components/SocialMedia'
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
title: 'Work',
|
||||
links: [
|
||||
{ title: 'FamilyFund', href: '/work/family-fund' },
|
||||
{ title: 'Unseal', href: '/work/unseal' },
|
||||
{ title: 'Phobia', href: '/work/phobia' },
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
See all <span aria-hidden="true">→</span>
|
||||
</>
|
||||
),
|
||||
href: '/work',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
links: [
|
||||
{ title: 'About', href: '/about' },
|
||||
{ title: 'Process', href: '/process' },
|
||||
{ title: 'Blog', href: '/blog' },
|
||||
{ title: 'Contact us', href: '/contact' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Connect',
|
||||
links: socialMediaProfiles,
|
||||
},
|
||||
]
|
||||
|
||||
function Navigation() {
|
||||
return (
|
||||
<nav>
|
||||
<ul role="list" className="grid grid-cols-2 gap-8 sm:grid-cols-3">
|
||||
{navigation.map((section, sectionIndex) => (
|
||||
<li key={sectionIndex}>
|
||||
<div className="font-display text-sm font-semibold tracking-wider text-neutral-950">
|
||||
{section.title}
|
||||
</div>
|
||||
<ul role="list" className="mt-4 text-sm text-neutral-700">
|
||||
{section.links.map((link, linkIndex) => (
|
||||
<li key={linkIndex} className="mt-4">
|
||||
<Link
|
||||
href={link.href}
|
||||
className="transition hover:text-neutral-950"
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
function ArrowIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 16 6" aria-hidden="true" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16 3 10 .5v2H0v1h10v2L16 3Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function NewsletterForm() {
|
||||
return (
|
||||
<form className="max-w-sm">
|
||||
<h2 className="font-display text-sm font-semibold tracking-wider text-neutral-950">
|
||||
Sign up for our newsletter
|
||||
</h2>
|
||||
<p className="mt-4 text-sm text-neutral-700">
|
||||
Subscribe to get the latest design news, articles, resources and
|
||||
inspiration.
|
||||
</p>
|
||||
<div className="relative mt-6">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email address"
|
||||
autoComplete="email"
|
||||
aria-label="Email address"
|
||||
className="block w-full rounded-2xl border border-neutral-300 bg-transparent py-4 pr-20 pl-6 text-base/6 text-neutral-950 ring-4 ring-transparent transition placeholder:text-neutral-500 focus:border-neutral-950 focus:ring-neutral-950/5 focus:outline-hidden"
|
||||
/>
|
||||
<div className="absolute inset-y-1 right-1 flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
aria-label="Submit"
|
||||
className="flex aspect-square h-full items-center justify-center rounded-xl bg-neutral-950 text-white transition hover:bg-neutral-800"
|
||||
>
|
||||
<ArrowIcon className="w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<Container as="footer" className="mt-24 w-full sm:mt-32 lg:mt-40">
|
||||
<FadeIn>
|
||||
<div className="grid grid-cols-1 gap-x-8 gap-y-16 lg:grid-cols-2">
|
||||
<Navigation />
|
||||
<div className="flex lg:justify-end">
|
||||
<NewsletterForm />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-24 mb-20 flex flex-wrap items-end justify-between gap-x-6 gap-y-4 border-t border-neutral-950/10 pt-12">
|
||||
<Link href="/" aria-label="Home">
|
||||
<Logo className="h-8" fillOnHover />
|
||||
</Link>
|
||||
<p className="text-sm text-neutral-700">
|
||||
© Studio Agency Inc. {new Date().getFullYear()}
|
||||
</p>
|
||||
</div>
|
||||
</FadeIn>
|
||||
</Container>
|
||||
)
|
||||
}
|
39
src/components/GrayscaleTransitionImage.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
import { useRef } from 'react'
|
||||
import Image, { type ImageProps } from 'next/image'
|
||||
import {
|
||||
motion,
|
||||
useMotionTemplate,
|
||||
useScroll,
|
||||
useTransform,
|
||||
} from 'framer-motion'
|
||||
|
||||
const MotionImage = motion(Image)
|
||||
|
||||
export function GrayscaleTransitionImage(
|
||||
props: Pick<
|
||||
ImageProps,
|
||||
'src' | 'quality' | 'className' | 'sizes' | 'priority'
|
||||
> & { alt?: string },
|
||||
) {
|
||||
let ref = useRef<React.ElementRef<'div'>>(null)
|
||||
let { scrollYProgress } = useScroll({
|
||||
target: ref,
|
||||
offset: ['start 65%', 'end 35%'],
|
||||
})
|
||||
let grayscale = useTransform(scrollYProgress, [0, 0.5, 1], [1, 0, 1])
|
||||
let filter = useMotionTemplate`grayscale(${grayscale})`
|
||||
|
||||
return (
|
||||
<div ref={ref} className="group relative">
|
||||
<MotionImage alt="" style={{ filter } as any} {...props} />
|
||||
<div
|
||||
className="pointer-events-none absolute top-0 left-0 w-full opacity-0 transition duration-300 group-hover:opacity-100"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Image alt="" {...props} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
64
src/components/GridList.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Border } from '@/components/Border'
|
||||
import { FadeIn, FadeInStagger } from '@/components/FadeIn'
|
||||
|
||||
export function GridList({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<FadeInStagger>
|
||||
<ul
|
||||
role="list"
|
||||
className={clsx(
|
||||
'grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-3',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
</FadeInStagger>
|
||||
)
|
||||
}
|
||||
|
||||
export function GridListItem({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
invert = false,
|
||||
}: {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
invert?: boolean
|
||||
}) {
|
||||
return (
|
||||
<li
|
||||
className={clsx(
|
||||
'text-base',
|
||||
invert
|
||||
? 'text-neutral-300 before:bg-white after:bg-white/10'
|
||||
: 'text-neutral-600 before:bg-neutral-950 after:bg-neutral-100',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<FadeIn>
|
||||
<Border position="left" className="pl-8" invert={invert}>
|
||||
<strong
|
||||
className={clsx(
|
||||
'font-semibold',
|
||||
invert ? 'text-white' : 'text-neutral-950',
|
||||
)}
|
||||
>
|
||||
{title}.
|
||||
</strong>{' '}
|
||||
{children}
|
||||
</Border>
|
||||
</FadeIn>
|
||||
</li>
|
||||
)
|
||||
}
|
129
src/components/GridPattern.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
function Block({
|
||||
x,
|
||||
y,
|
||||
...props
|
||||
}: Omit<React.ComponentPropsWithoutRef<typeof motion.path>, 'x' | 'y'> & {
|
||||
x: number
|
||||
y: number
|
||||
}) {
|
||||
return (
|
||||
<motion.path
|
||||
transform={`translate(${-32 * y + 96 * x} ${160 * y})`}
|
||||
d="M45.119 4.5a11.5 11.5 0 0 0-11.277 9.245l-25.6 128C6.82 148.861 12.262 155.5 19.52 155.5h63.366a11.5 11.5 0 0 0 11.277-9.245l25.6-128c1.423-7.116-4.02-13.755-11.277-13.755H45.119Z"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function GridPattern({
|
||||
yOffset = 0,
|
||||
interactive = false,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'svg'> & {
|
||||
yOffset?: number
|
||||
interactive?: boolean
|
||||
}) {
|
||||
let id = useId()
|
||||
let ref = useRef<React.ElementRef<'svg'>>(null)
|
||||
let currentBlock = useRef<[x: number, y: number]>()
|
||||
let counter = useRef(0)
|
||||
let [hoveredBlocks, setHoveredBlocks] = useState<
|
||||
Array<[x: number, y: number, key: number]>
|
||||
>([])
|
||||
let staticBlocks = [
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[4, 3],
|
||||
[6, 2],
|
||||
[7, 4],
|
||||
[5, 5],
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
if (!interactive) {
|
||||
return
|
||||
}
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
if (!ref.current) {
|
||||
return
|
||||
}
|
||||
|
||||
let rect = ref.current.getBoundingClientRect()
|
||||
let x = event.clientX - rect.left
|
||||
let y = event.clientY - rect.top
|
||||
if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
|
||||
return
|
||||
}
|
||||
|
||||
x = x - rect.width / 2 - 32
|
||||
y = y - yOffset
|
||||
x += Math.tan(32 / 160) * y
|
||||
x = Math.floor(x / 96)
|
||||
y = Math.floor(y / 160)
|
||||
|
||||
if (currentBlock.current?.[0] === x && currentBlock.current?.[1] === y) {
|
||||
return
|
||||
}
|
||||
|
||||
currentBlock.current = [x, y]
|
||||
|
||||
setHoveredBlocks((blocks) => {
|
||||
let key = counter.current++
|
||||
let block = [x, y, key] as (typeof hoveredBlocks)[number]
|
||||
return [...blocks, block].filter(
|
||||
(block) => !(block[0] === x && block[1] === y && block[2] !== key),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
}
|
||||
}, [yOffset, interactive])
|
||||
|
||||
return (
|
||||
<svg ref={ref} aria-hidden="true" {...props}>
|
||||
<rect width="100%" height="100%" fill={`url(#${id})`} strokeWidth="0" />
|
||||
<svg x="50%" y={yOffset} strokeWidth="0" className="overflow-visible">
|
||||
{staticBlocks.map((block) => (
|
||||
<Block key={`${block}`} x={block[0]} y={block[1]} />
|
||||
))}
|
||||
{hoveredBlocks.map((block) => (
|
||||
<Block
|
||||
key={block[2]}
|
||||
x={block[0]}
|
||||
y={block[1]}
|
||||
animate={{ opacity: [0, 1, 0] }}
|
||||
transition={{ duration: 1, times: [0, 0, 1] }}
|
||||
onAnimationComplete={() => {
|
||||
setHoveredBlocks((blocks) =>
|
||||
blocks.filter((b) => b[2] !== block[2]),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
<defs>
|
||||
<pattern
|
||||
id={id}
|
||||
width="96"
|
||||
height="480"
|
||||
x="50%"
|
||||
patternUnits="userSpaceOnUse"
|
||||
patternTransform={`translate(0 ${yOffset})`}
|
||||
fill="none"
|
||||
>
|
||||
<path d="M128 0 98.572 147.138A16 16 0 0 1 82.883 160H13.117a16 16 0 0 0-15.69 12.862l-26.855 134.276A16 16 0 0 1-45.117 320H-116M64-160 34.572-12.862A16 16 0 0 1 18.883 0h-69.766a16 16 0 0 0-15.69 12.862l-26.855 134.276A16 16 0 0 1-109.117 160H-180M192 160l-29.428 147.138A15.999 15.999 0 0 1 146.883 320H77.117a16 16 0 0 0-15.69 12.862L34.573 467.138A16 16 0 0 1 18.883 480H-52M-136 480h58.883a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1-18.883 320h69.766a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1 109.117 160H192M-72 640h58.883a16 16 0 0 0 15.69-12.862l26.855-134.276A16 16 0 0 1 45.117 480h69.766a15.999 15.999 0 0 0 15.689-12.862l26.856-134.276A15.999 15.999 0 0 1 173.117 320H256M-200 320h58.883a15.999 15.999 0 0 0 15.689-12.862l26.856-134.276A16 16 0 0 1-82.883 160h69.766a16 16 0 0 0 15.69-12.862L29.427 12.862A16 16 0 0 1 45.117 0H128" />
|
||||
</pattern>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
41
src/components/List.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Border } from '@/components/Border'
|
||||
import { FadeIn, FadeInStagger } from '@/components/FadeIn'
|
||||
|
||||
export function List({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<FadeInStagger>
|
||||
<ul role="list" className={clsx('text-base text-neutral-600', className)}>
|
||||
{children}
|
||||
</ul>
|
||||
</FadeInStagger>
|
||||
)
|
||||
}
|
||||
|
||||
export function ListItem({
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
title?: string
|
||||
}) {
|
||||
return (
|
||||
<li className="group mt-10 first:mt-0">
|
||||
<FadeIn>
|
||||
<Border className="pt-10 group-first:pt-0 group-first:before:hidden group-first:after:hidden">
|
||||
{title && (
|
||||
<strong className="font-semibold text-neutral-950">{`${title}. `}</strong>
|
||||
)}
|
||||
{children}
|
||||
</Border>
|
||||
</FadeIn>
|
||||
</li>
|
||||
)
|
||||
}
|
72
src/components/Logo.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { useId } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export function Logomark({
|
||||
invert = false,
|
||||
filled = false,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'svg'> & {
|
||||
invert?: boolean
|
||||
filled?: boolean
|
||||
}) {
|
||||
let id = useId()
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 32 32" aria-hidden="true" {...props}>
|
||||
<rect
|
||||
clipPath={`url(#${id}-clip)`}
|
||||
className={clsx(
|
||||
'h-8 transition-all duration-300',
|
||||
invert ? 'fill-white' : 'fill-neutral-950',
|
||||
filled ? 'w-8' : 'w-0 group-hover/logo:w-8',
|
||||
)}
|
||||
/>
|
||||
<use
|
||||
href={`#${id}-path`}
|
||||
className={invert ? 'stroke-white' : 'stroke-neutral-950'}
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<defs>
|
||||
<path
|
||||
id={`${id}-path`}
|
||||
d="M3.25 26v.75H7c1.305 0 2.384-.21 3.346-.627.96-.415 1.763-1.02 2.536-1.752.695-.657 1.39-1.443 2.152-2.306l.233-.263c.864-.975 1.843-2.068 3.071-3.266 1.209-1.18 2.881-1.786 4.621-1.786h5.791V5.25H25c-1.305 0-2.384.21-3.346.627-.96.415-1.763 1.02-2.536 1.751-.695.658-1.39 1.444-2.152 2.307l-.233.263c-.864.975-1.843 2.068-3.071 3.266-1.209 1.18-2.881 1.786-4.621 1.786H3.25V26Z"
|
||||
/>
|
||||
<clipPath id={`${id}-clip`}>
|
||||
<use href={`#${id}-path`} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Logo({
|
||||
className,
|
||||
invert = false,
|
||||
filled = false,
|
||||
fillOnHover = false,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'svg'> & {
|
||||
invert?: boolean
|
||||
filled?: boolean
|
||||
fillOnHover?: boolean
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 130 32"
|
||||
aria-hidden="true"
|
||||
className={clsx(fillOnHover && 'group/logo', className)}
|
||||
{...props}
|
||||
>
|
||||
<Logomark
|
||||
preserveAspectRatio="xMinYMid meet"
|
||||
invert={invert}
|
||||
filled={filled}
|
||||
/>
|
||||
<path
|
||||
className={invert ? 'fill-white' : 'fill-neutral-950'}
|
||||
d="M52.928 23.716c5.184 0 7.992-1.992 7.992-5.28 0-3.888-2.688-4.8-7.512-5.376-3.36-.408-4.728-.672-4.728-2.448 0-1.464 1.44-2.376 3.912-2.376 2.4 0 3.936.864 4.104 2.784h3.576c-.24-3.288-3-5.232-7.536-5.232-4.728 0-7.68 1.896-7.68 5.208 0 3.48 2.712 4.464 7.416 5.04 3.216.408 4.8.648 4.8 2.664 0 1.584-1.392 2.544-4.224 2.544-3.048 0-4.68-1.176-4.752-3.288H44.6c.072 3.408 2.616 5.76 8.328 5.76Zm14.175-.216h3.312v-2.928h-1.968c-.84 0-1.272-.24-1.272-1.104v-6.144h3.24v-2.592h-3.24V6.676l-3.36.648v3.408h-2.496v2.592h2.496v7.2c0 2.04 1.248 2.976 3.288 2.976Zm10.078.216c2.16 0 4.104-1.008 4.944-2.64h.168l.144 2.424h3.288V10.732h-3.432v6.336c0 2.4-1.584 4.032-3.984 4.032-2.328 0-3.264-1.368-3.264-3.936v-6.432h-3.432v7.032c0 4.416 2.256 5.952 5.568 5.952Zm16.24.048c2.52 0 4.2-1.008 4.944-2.496h.168l.072 2.232h3.264V6.004h-3.408v7.008h-.168c-.792-1.56-2.592-2.52-4.848-2.52-3.816 0-6.384 2.592-6.384 6.624 0 4.056 2.568 6.648 6.36 6.648Zm1.032-2.616c-2.472 0-3.96-1.536-3.96-4.032s1.488-4.008 3.96-4.008 3.984 1.512 3.984 3.648v.744c0 2.136-1.536 3.648-3.984 3.648Zm9.485-12.216h3.408V6.004h-3.408v2.928Zm0 14.568h3.408V10.732h-3.408V23.5Zm12.481.24c4.584 0 7.56-2.52 7.56-6.624 0-4.152-3-6.624-7.56-6.624s-7.56 2.52-7.56 6.624c0 4.128 3.024 6.624 7.56 6.624Zm0-2.64c-2.592 0-4.128-1.56-4.128-3.984s1.536-3.984 4.128-3.984c2.616 0 4.152 1.536 4.152 3.984 0 2.424-1.56 3.984-4.152 3.984Zm8.794 2.4h3.384v-2.88h-3.384v2.88Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
98
src/components/MDXComponents.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Blockquote } from '@/components/Blockquote'
|
||||
import { Border } from '@/components/Border'
|
||||
import { GrayscaleTransitionImage } from '@/components/GrayscaleTransitionImage'
|
||||
import { StatList, StatListItem } from '@/components/StatList'
|
||||
import { TagList, TagListItem } from '@/components/TagList'
|
||||
|
||||
export const MDXComponents = {
|
||||
Blockquote({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof Blockquote>) {
|
||||
return <Blockquote className={clsx('my-32', className)} {...props} />
|
||||
},
|
||||
img: function Img({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof GrayscaleTransitionImage>) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'group isolate my-10 overflow-hidden rounded-4xl bg-neutral-100 max-sm:-mx-6',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<GrayscaleTransitionImage
|
||||
{...props}
|
||||
sizes="(min-width: 768px) 42rem, 100vw"
|
||||
className="aspect-16/10 w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
StatList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof StatList>) {
|
||||
return (
|
||||
<StatList className={clsx('my-32 max-w-none!', className)} {...props} />
|
||||
)
|
||||
},
|
||||
StatListItem,
|
||||
table: function Table({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'table'>) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'my-10 max-sm:-mx-6 max-sm:flex max-sm:overflow-x-auto',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="max-sm:min-w-full max-sm:flex-none max-sm:px-6">
|
||||
<table {...props} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
TagList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof TagList>) {
|
||||
return <TagList className={clsx('my-6', className)} {...props} />
|
||||
},
|
||||
TagListItem,
|
||||
TopTip({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Border position="left" className={clsx('my-10 pl-8', className)}>
|
||||
<p className="font-display text-sm font-bold tracking-widest text-neutral-950 uppercase">
|
||||
Top tip
|
||||
</p>
|
||||
<div className="mt-4">{children}</div>
|
||||
</Border>
|
||||
)
|
||||
},
|
||||
Typography({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
return <div className={clsx('typography', className)} {...props} />
|
||||
},
|
||||
wrapper({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'*:mx-auto *:max-w-3xl [&>:first-child]:mt-0! [&>:last-child]:mb-0!',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
50
src/components/Offices.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
function Office({
|
||||
name,
|
||||
children,
|
||||
invert = false,
|
||||
}: {
|
||||
name: string
|
||||
children: React.ReactNode
|
||||
invert?: boolean
|
||||
}) {
|
||||
return (
|
||||
<address
|
||||
className={clsx(
|
||||
'text-sm not-italic',
|
||||
invert ? 'text-neutral-300' : 'text-neutral-600',
|
||||
)}
|
||||
>
|
||||
<strong className={invert ? 'text-white' : 'text-neutral-950'}>
|
||||
{name}
|
||||
</strong>
|
||||
<br />
|
||||
{children}
|
||||
</address>
|
||||
)
|
||||
}
|
||||
|
||||
export function Offices({
|
||||
invert = false,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'ul'> & { invert?: boolean }) {
|
||||
return (
|
||||
<ul role="list" {...props}>
|
||||
<li>
|
||||
<Office name="Copenhagen" invert={invert}>
|
||||
1 Carlsberg Gate
|
||||
<br />
|
||||
1260, København, Denmark
|
||||
</Office>
|
||||
</li>
|
||||
<li>
|
||||
<Office name="Billund" invert={invert}>
|
||||
24 Lego Allé
|
||||
<br />
|
||||
7190, Billund, Denmark
|
||||
</Office>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
47
src/components/PageIntro.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn } from '@/components/FadeIn'
|
||||
|
||||
export function PageIntro({
|
||||
eyebrow,
|
||||
title,
|
||||
children,
|
||||
centered = false,
|
||||
}: {
|
||||
eyebrow: string
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
centered?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Container
|
||||
className={clsx('mt-24 sm:mt-32 lg:mt-40', centered && 'text-center')}
|
||||
>
|
||||
<FadeIn>
|
||||
<h1>
|
||||
<span className="block font-display text-base font-semibold text-neutral-950">
|
||||
{eyebrow}
|
||||
</span>
|
||||
<span className="sr-only"> - </span>
|
||||
<span
|
||||
className={clsx(
|
||||
'mt-6 block max-w-5xl font-display text-5xl font-medium tracking-tight text-balance text-neutral-950 sm:text-6xl',
|
||||
centered && 'mx-auto',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</h1>
|
||||
<div
|
||||
className={clsx(
|
||||
'mt-6 max-w-3xl text-xl text-neutral-600',
|
||||
centered && 'mx-auto',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</FadeIn>
|
||||
</Container>
|
||||
)
|
||||
}
|
96
src/components/PageLinks.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Border } from '@/components/Border'
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn, FadeInStagger } from '@/components/FadeIn'
|
||||
import { GridPattern } from '@/components/GridPattern'
|
||||
import { SectionIntro } from '@/components/SectionIntro'
|
||||
import { formatDate } from '@/lib/formatDate'
|
||||
|
||||
function ArrowIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 6" aria-hidden="true" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M24 3 18 .5v2H0v1h18v2L24 3Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
interface Page {
|
||||
href: string
|
||||
date: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
function PageLink({ page }: { page: Page }) {
|
||||
return (
|
||||
<article key={page.href}>
|
||||
<Border
|
||||
position="left"
|
||||
className="relative flex flex-col items-start pl-8"
|
||||
>
|
||||
<h3 className="mt-6 text-base font-semibold text-neutral-950">
|
||||
{page.title}
|
||||
</h3>
|
||||
<time
|
||||
dateTime={page.date}
|
||||
className="order-first text-sm text-neutral-600"
|
||||
>
|
||||
{formatDate(page.date)}
|
||||
</time>
|
||||
<p className="mt-2.5 text-base text-neutral-600">{page.description}</p>
|
||||
<Link
|
||||
href={page.href}
|
||||
className="mt-6 flex gap-x-3 text-base font-semibold text-neutral-950 transition hover:text-neutral-700"
|
||||
aria-label={`Read more: ${page.title}`}
|
||||
>
|
||||
Read more
|
||||
<ArrowIcon className="w-6 flex-none fill-current" />
|
||||
<span className="absolute inset-0" />
|
||||
</Link>
|
||||
</Border>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export function PageLinks({
|
||||
title,
|
||||
pages,
|
||||
intro,
|
||||
className,
|
||||
}: {
|
||||
title: string
|
||||
pages: Array<Page>
|
||||
intro?: string
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={clsx('relative pt-24 sm:pt-32 lg:pt-40', className)}>
|
||||
<div className="absolute inset-x-0 top-0 -z-10 h-[884px] overflow-hidden rounded-t-4xl bg-linear-to-b from-neutral-50">
|
||||
<GridPattern
|
||||
className="absolute inset-0 h-full w-full mask-[linear-gradient(to_bottom_left,white_40%,transparent_50%)] fill-neutral-100 stroke-neutral-950/5"
|
||||
yOffset={-270}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SectionIntro title={title} smaller>
|
||||
{intro && <p>{intro}</p>}
|
||||
</SectionIntro>
|
||||
|
||||
<Container className={intro ? 'mt-24' : 'mt-16'}>
|
||||
<FadeInStagger className="grid grid-cols-1 gap-x-8 gap-y-16 lg:grid-cols-2">
|
||||
{pages.map((page) => (
|
||||
<FadeIn key={page.href}>
|
||||
<PageLink page={page} />
|
||||
</FadeIn>
|
||||
))}
|
||||
</FadeInStagger>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
297
src/components/RootLayout.tsx
Normal file
@ -0,0 +1,297 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import clsx from 'clsx'
|
||||
import { motion, MotionConfig, useReducedMotion } from 'framer-motion'
|
||||
|
||||
import { Button } from '@/components/Button'
|
||||
import { Container } from '@/components/Container'
|
||||
import { Footer } from '@/components/Footer'
|
||||
import { GridPattern } from '@/components/GridPattern'
|
||||
import { Logo, Logomark } from '@/components/Logo'
|
||||
import { Offices } from '@/components/Offices'
|
||||
import { SocialMedia } from '@/components/SocialMedia'
|
||||
|
||||
const RootLayoutContext = createContext<{
|
||||
logoHovered: boolean
|
||||
setLogoHovered: React.Dispatch<React.SetStateAction<boolean>>
|
||||
} | null>(null)
|
||||
|
||||
function XIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path d="m5.636 4.223 14.142 14.142-1.414 1.414L4.222 5.637z" />
|
||||
<path d="M4.222 18.363 18.364 4.22l1.414 1.414L5.636 19.777z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MenuIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path d="M2 6h20v2H2zM2 16h20v2H2z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function Header({
|
||||
panelId,
|
||||
icon: Icon,
|
||||
expanded,
|
||||
onToggle,
|
||||
toggleRef,
|
||||
invert = false,
|
||||
}: {
|
||||
panelId: string
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
expanded: boolean
|
||||
onToggle: () => void
|
||||
toggleRef: React.RefObject<HTMLButtonElement>
|
||||
invert?: boolean
|
||||
}) {
|
||||
let { logoHovered, setLogoHovered } = useContext(RootLayoutContext)!
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="Home"
|
||||
onMouseEnter={() => setLogoHovered(true)}
|
||||
onMouseLeave={() => setLogoHovered(false)}
|
||||
>
|
||||
<Logomark
|
||||
className="h-8 sm:hidden"
|
||||
invert={invert}
|
||||
filled={logoHovered}
|
||||
/>
|
||||
<Logo
|
||||
className="hidden h-8 sm:block"
|
||||
invert={invert}
|
||||
filled={logoHovered}
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex items-center gap-x-8">
|
||||
<Button href="/contact" invert={invert}>
|
||||
Contact us
|
||||
</Button>
|
||||
<button
|
||||
ref={toggleRef}
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
aria-expanded={expanded ? 'true' : 'false'}
|
||||
aria-controls={panelId}
|
||||
className={clsx(
|
||||
'group -m-2.5 rounded-full p-2.5 transition',
|
||||
invert ? 'hover:bg-white/10' : 'hover:bg-neutral-950/10',
|
||||
)}
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<Icon
|
||||
className={clsx(
|
||||
'h-6 w-6',
|
||||
invert
|
||||
? 'fill-white group-hover:fill-neutral-200'
|
||||
: 'fill-neutral-950 group-hover:fill-neutral-700',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationRow({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="even:mt-px sm:bg-neutral-950">
|
||||
<Container>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2">{children}</div>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationItem({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="group relative isolate -mx-6 bg-neutral-950 px-6 py-10 even:mt-px sm:mx-0 sm:px-0 sm:py-16 sm:odd:pr-16 sm:even:mt-0 sm:even:border-l sm:even:border-neutral-800 sm:even:pl-16"
|
||||
>
|
||||
{children}
|
||||
<span className="absolute inset-y-0 -z-10 w-screen bg-neutral-900 opacity-0 transition group-odd:right-0 group-even:left-0 group-hover:opacity-100" />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function Navigation() {
|
||||
return (
|
||||
<nav className="mt-px font-display text-5xl font-medium tracking-tight text-white">
|
||||
<NavigationRow>
|
||||
<NavigationItem href="/work">Our Work</NavigationItem>
|
||||
<NavigationItem href="/about">About Us</NavigationItem>
|
||||
</NavigationRow>
|
||||
<NavigationRow>
|
||||
<NavigationItem href="/process">Our Process</NavigationItem>
|
||||
<NavigationItem href="/blog">Blog</NavigationItem>
|
||||
</NavigationRow>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
function RootLayoutInner({ children }: { children: React.ReactNode }) {
|
||||
let panelId = useId()
|
||||
let [expanded, setExpanded] = useState(false)
|
||||
let [isTransitioning, setIsTransitioning] = useState(false)
|
||||
let openRef = useRef<React.ElementRef<'button'>>(null)
|
||||
let closeRef = useRef<React.ElementRef<'button'>>(null)
|
||||
let navRef = useRef<React.ElementRef<'div'>>(null)
|
||||
let shouldReduceMotion = useReducedMotion()
|
||||
|
||||
useEffect(() => {
|
||||
function onClick(event: MouseEvent) {
|
||||
if (
|
||||
event.target instanceof HTMLElement &&
|
||||
event.target.closest('a')?.href === window.location.href
|
||||
) {
|
||||
setIsTransitioning(false)
|
||||
setExpanded(false)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('click', onClick)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', onClick)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<MotionConfig
|
||||
transition={
|
||||
shouldReduceMotion || !isTransitioning ? { duration: 0 } : undefined
|
||||
}
|
||||
>
|
||||
<header>
|
||||
<div
|
||||
className="absolute top-2 right-0 left-0 z-40 pt-14"
|
||||
aria-hidden={expanded ? 'true' : undefined}
|
||||
// @ts-ignore (https://github.com/facebook/react/issues/17157)
|
||||
inert={expanded ? '' : undefined}
|
||||
>
|
||||
<Header
|
||||
panelId={panelId}
|
||||
icon={MenuIcon}
|
||||
toggleRef={openRef}
|
||||
expanded={expanded}
|
||||
onToggle={() => {
|
||||
setIsTransitioning(true)
|
||||
setExpanded((expanded) => !expanded)
|
||||
window.setTimeout(() =>
|
||||
closeRef.current?.focus({ preventScroll: true }),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
layout
|
||||
id={panelId}
|
||||
style={{ height: expanded ? 'auto' : '0.5rem' }}
|
||||
className="relative z-50 overflow-hidden bg-neutral-950 pt-2"
|
||||
aria-hidden={expanded ? undefined : 'true'}
|
||||
// @ts-ignore (https://github.com/facebook/react/issues/17157)
|
||||
inert={expanded ? undefined : ''}
|
||||
>
|
||||
<motion.div layout className="bg-neutral-800">
|
||||
<div ref={navRef} className="bg-neutral-950 pt-14 pb-16">
|
||||
<Header
|
||||
invert
|
||||
panelId={panelId}
|
||||
icon={XIcon}
|
||||
toggleRef={closeRef}
|
||||
expanded={expanded}
|
||||
onToggle={() => {
|
||||
setIsTransitioning(true)
|
||||
setExpanded((expanded) => !expanded)
|
||||
window.setTimeout(() =>
|
||||
openRef.current?.focus({ preventScroll: true }),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Navigation />
|
||||
<div className="relative bg-neutral-950 before:absolute before:inset-x-0 before:top-0 before:h-px before:bg-neutral-800">
|
||||
<Container>
|
||||
<div className="grid grid-cols-1 gap-y-10 pt-10 pb-16 sm:grid-cols-2 sm:pt-16">
|
||||
<div>
|
||||
<h2 className="font-display text-base font-semibold text-white">
|
||||
Our offices
|
||||
</h2>
|
||||
<Offices
|
||||
invert
|
||||
className="mt-6 grid grid-cols-1 gap-8 sm:grid-cols-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:border-l sm:border-transparent sm:pl-16">
|
||||
<h2 className="font-display text-base font-semibold text-white">
|
||||
Follow us
|
||||
</h2>
|
||||
<SocialMedia className="mt-6" invert />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</header>
|
||||
|
||||
<motion.div
|
||||
layout
|
||||
style={{ borderTopLeftRadius: 40, borderTopRightRadius: 40 }}
|
||||
className="relative flex flex-auto overflow-hidden bg-white pt-14"
|
||||
>
|
||||
<motion.div
|
||||
layout
|
||||
className="relative isolate flex w-full flex-col pt-9"
|
||||
>
|
||||
<GridPattern
|
||||
className="absolute inset-x-0 -top-14 -z-10 h-[1000px] w-full mask-[linear-gradient(to_bottom_left,white_40%,transparent_50%)] fill-neutral-50 stroke-neutral-950/5"
|
||||
yOffset={-96}
|
||||
interactive
|
||||
/>
|
||||
|
||||
<main className="w-full flex-auto">{children}</main>
|
||||
|
||||
<Footer />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</MotionConfig>
|
||||
)
|
||||
}
|
||||
|
||||
export function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
let pathname = usePathname()
|
||||
let [logoHovered, setLogoHovered] = useState(false)
|
||||
|
||||
return (
|
||||
<RootLayoutContext.Provider value={{ logoHovered, setLogoHovered }}>
|
||||
<RootLayoutInner key={pathname}>{children}</RootLayoutInner>
|
||||
</RootLayoutContext.Provider>
|
||||
)
|
||||
}
|
65
src/components/SectionIntro.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn } from '@/components/FadeIn'
|
||||
|
||||
export function SectionIntro({
|
||||
title,
|
||||
eyebrow,
|
||||
children,
|
||||
smaller = false,
|
||||
invert = false,
|
||||
...props
|
||||
}: Omit<
|
||||
React.ComponentPropsWithoutRef<typeof Container>,
|
||||
'title' | 'children'
|
||||
> & {
|
||||
title: string
|
||||
eyebrow?: string
|
||||
children?: React.ReactNode
|
||||
smaller?: boolean
|
||||
invert?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Container {...props}>
|
||||
<FadeIn className="max-w-2xl">
|
||||
<h2>
|
||||
{eyebrow && (
|
||||
<>
|
||||
<span
|
||||
className={clsx(
|
||||
'mb-6 block font-display text-base font-semibold',
|
||||
invert ? 'text-white' : 'text-neutral-950',
|
||||
)}
|
||||
>
|
||||
{eyebrow}
|
||||
</span>
|
||||
<span className="sr-only"> - </span>
|
||||
</>
|
||||
)}
|
||||
<span
|
||||
className={clsx(
|
||||
'block font-display tracking-tight text-balance',
|
||||
smaller
|
||||
? 'text-2xl font-semibold'
|
||||
: 'text-4xl font-medium sm:text-5xl',
|
||||
invert ? 'text-white' : 'text-neutral-950',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</h2>
|
||||
{children && (
|
||||
<div
|
||||
className={clsx(
|
||||
'mt-6 text-xl',
|
||||
invert ? 'text-neutral-300' : 'text-neutral-600',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</FadeIn>
|
||||
</Container>
|
||||
)
|
||||
}
|
91
src/components/SocialMedia.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
function FacebookIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function InstagramIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465.668.25 1.272.644 1.772 1.153.509.5.902 1.104 1.153 1.772.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.903 4.903 0 0 1-1.153 1.772c-.5.509-1.104.902-1.772 1.153-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.903 4.903 0 0 1-1.772-1.153 4.902 4.902 0 0 1-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 0 1 1.153-1.772A4.902 4.902 0 0 1 5.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63Zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.096 3.096 0 0 0-.748-1.15 3.098 3.098 0 0 0-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058ZM12 6.865a5.135 5.135 0 1 1 0 10.27 5.135 5.135 0 0 1 0-10.27Zm0 1.802a3.333 3.333 0 1 0 0 6.666 3.333 3.333 0 0 0 0-6.666Zm5.338-3.205a1.2 1.2 0 1 1 0 2.4 1.2 1.2 0 0 1 0-2.4Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function GitHubIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function DribbbleIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c5.51 0 10-4.48 10-10S17.51 2 12 2Zm6.605 4.61a8.502 8.502 0 0 1 1.93 5.314c-.281-.054-3.101-.629-5.943-.271-.065-.141-.12-.293-.184-.445a25.42 25.42 0 0 0-.564-1.236c3.145-1.28 4.577-3.124 4.761-3.362ZM12 3.475c2.17 0 4.154.813 5.662 2.148-.152.216-1.443 1.941-4.48 3.08-1.399-2.57-2.95-4.675-3.189-5A8.688 8.688 0 0 1 12 3.475Zm-3.633.803a53.889 53.889 0 0 1 3.167 4.935c-3.992 1.063-7.517 1.04-7.896 1.04a8.581 8.581 0 0 1 4.729-5.975ZM3.453 12.01v-.26c.37.01 4.512.065 8.775-1.215.25.477.477.965.694 1.453-.109.033-.228.065-.336.098-4.404 1.42-6.747 5.303-6.942 5.629a8.523 8.523 0 0 1-2.191-5.705ZM12 20.547a8.482 8.482 0 0 1-5.239-1.8c.152-.315 1.888-3.656 6.703-5.337.022-.01.033-.01.054-.022a35.32 35.32 0 0 1 1.823 6.475 8.402 8.402 0 0 1-3.341.684Zm4.761-1.465c-.086-.52-.542-3.015-1.659-6.084 2.679-.423 5.022.271 5.314.369a8.468 8.468 0 0 1-3.655 5.715Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const socialMediaProfiles = [
|
||||
{ title: 'Facebook', href: 'https://facebook.com', icon: FacebookIcon },
|
||||
{ title: 'Instagram', href: 'https://instagram.com', icon: InstagramIcon },
|
||||
{ title: 'GitHub', href: 'https://github.com', icon: GitHubIcon },
|
||||
{ title: 'Dribbble', href: 'https://dribbble.com', icon: DribbbleIcon },
|
||||
]
|
||||
|
||||
export function SocialMedia({
|
||||
className,
|
||||
invert = false,
|
||||
}: {
|
||||
className?: string
|
||||
invert?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ul
|
||||
role="list"
|
||||
className={clsx(
|
||||
'flex gap-x-10',
|
||||
invert ? 'text-white' : 'text-neutral-950',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{socialMediaProfiles.map((socialMediaProfile) => (
|
||||
<li key={socialMediaProfile.title}>
|
||||
<Link
|
||||
href={socialMediaProfile.href}
|
||||
aria-label={socialMediaProfile.title}
|
||||
className={clsx(
|
||||
'transition',
|
||||
invert ? 'hover:text-neutral-200' : 'hover:text-neutral-700',
|
||||
)}
|
||||
>
|
||||
<socialMediaProfile.icon className="h-6 w-6 fill-current" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
34
src/components/StatList.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Border } from '@/components/Border'
|
||||
import { FadeIn, FadeInStagger } from '@/components/FadeIn'
|
||||
|
||||
export function StatList({
|
||||
children,
|
||||
...props
|
||||
}: Omit<React.ComponentPropsWithoutRef<typeof FadeInStagger>, 'children'> & {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<FadeInStagger {...props}>
|
||||
<dl className="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:auto-cols-fr lg:grid-flow-col lg:grid-cols-none">
|
||||
{children}
|
||||
</dl>
|
||||
</FadeInStagger>
|
||||
)
|
||||
}
|
||||
|
||||
export function StatListItem({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
}) {
|
||||
return (
|
||||
<Border as={FadeIn} position="left" className="flex flex-col-reverse pl-8">
|
||||
<dt className="mt-2 text-base text-neutral-600">{label}</dt>
|
||||
<dd className="font-display text-3xl font-semibold text-neutral-950 sm:text-4xl">
|
||||
{value}
|
||||
</dd>
|
||||
</Border>
|
||||
)
|
||||
}
|
71
src/components/StylizedImage.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { useId } from 'react'
|
||||
import Image, { type ImageProps } from 'next/image'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const shapes = [
|
||||
{
|
||||
width: 655,
|
||||
height: 680,
|
||||
path: 'M537.827 9.245A11.5 11.5 0 0 1 549.104 0h63.366c7.257 0 12.7 6.64 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 586.87 151h-28.275a15.999 15.999 0 0 0-15.689 12.862l-59.4 297c-1.98 9.901 5.592 19.138 15.689 19.138h17.275l.127.001c.85.009 1.701.074 2.549.009 11.329-.874 21.411-7.529 24.88-25.981.002-.012.016-.016.023-.007.008.009.022.005.024-.006l24.754-123.771A11.5 11.5 0 0 1 580.104 321h63.366c7.257 0 12.7 6.639 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 617.87 472H559c-22.866 0-28.984 7.98-31.989 25.931-.004.026-.037.035-.052.014-.015-.02-.048-.013-.053.012l-24.759 123.798A11.5 11.5 0 0 1 490.87 631h-29.132a14.953 14.953 0 0 0-14.664 12.021c-4.3 21.502-23.18 36.979-45.107 36.979H83.502c-29.028 0-50.8-26.557-45.107-55.021l102.4-512C145.096 91.477 163.975 76 185.902 76h318.465c10.136 0 21.179-5.35 23.167-15.288l10.293-51.467Zm-512 160A11.5 11.5 0 0 1 37.104 160h63.366c7.257 0 12.7 6.639 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 74.87 311H11.504c-7.257 0-12.7-6.639-11.277-13.755l25.6-128Z',
|
||||
},
|
||||
{
|
||||
width: 719,
|
||||
height: 680,
|
||||
path: 'M89.827 9.245A11.5 11.5 0 0 1 101.104 0h63.366c7.257 0 12.7 6.64 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 138.87 151H75.504c-7.257 0-12.7-6.639-11.277-13.755l25.6-128Zm-64 321A11.5 11.5 0 0 1 37.104 321h63.366c7.257 0 12.7 6.639 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 74.87 472H11.504c-7.257 0-12.7-6.639-11.277-13.755l25.6-128ZM526.795 470a15.999 15.999 0 0 0-15.689 12.862l-32.032 160.159c-4.3 21.502-23.18 36.979-45.107 36.979H115.502c-29.028 0-50.8-26.557-45.107-55.021l102.4-512C177.096 91.477 195.975 76 217.902 76h318.465c29.028 0 50.8 26.557 45.107 55.021l-33.768 168.841c-1.98 9.901 5.592 19.138 15.689 19.138h17.075l.127.001c.85.009 1.701.074 2.549.009 11.329-.874 21.411-7.529 24.88-25.981.002-.012.016-.016.023-.007.008.009.022.005.024-.006l24.754-123.771A11.5 11.5 0 0 1 644.104 160h63.366c7.257 0 12.7 6.639 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 681.87 311H623c-22.866 0-28.984 7.98-31.989 25.931-.004.026-.037.035-.052.014-.015-.02-.048-.013-.053.012l-24.759 123.798A11.5 11.5 0 0 1 554.87 470h-28.075Z',
|
||||
},
|
||||
{
|
||||
width: 719,
|
||||
height: 680,
|
||||
path: 'M632.827 9.245A11.5 11.5 0 0 1 644.104 0h63.366c7.257 0 12.7 6.64 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 681.87 151h-28.275a15.999 15.999 0 0 0-15.689 12.862l-95.832 479.159c-4.3 21.502-23.18 36.979-45.107 36.979H178.502c-29.028 0-50.8-26.557-45.107-55.021l102.4-512C240.096 91.477 258.975 76 280.902 76h318.465c10.136 0 21.179-5.35 23.167-15.288l10.293-51.467Zm0 479A11.5 11.5 0 0 1 644.104 479h63.366c7.257 0 12.7 6.639 11.277 13.755l-25.6 128A11.5 11.5 0 0 1 681.87 630h-63.366c-7.257 0-12.7-6.639-11.277-13.755l25.6-128ZM37.104 159a11.5 11.5 0 0 0-11.277 9.245l-25.6 128C-1.196 303.361 4.247 310 11.504 310H74.87a11.5 11.5 0 0 0 11.277-9.245l24.76-123.798a.03.03 0 0 1 .052-.012c.015.021.048.012.052-.014C114.016 158.98 120.134 151 143 151h58.87a11.5 11.5 0 0 0 11.277-9.245l25.6-128C240.17 6.64 234.727 0 227.47 0h-63.366a11.5 11.5 0 0 0-11.277 9.245l-24.754 123.771c-.002.011-.016.015-.024.006-.007-.009-.021-.005-.023.007-3.469 18.452-13.551 25.107-24.88 25.981-.848.065-1.699 0-2.549-.009l-.127-.001H37.104Z',
|
||||
},
|
||||
]
|
||||
|
||||
type ImagePropsWithOptionalAlt = Omit<ImageProps, 'alt'> & { alt?: string }
|
||||
|
||||
export function StylizedImage({
|
||||
shape = 0,
|
||||
className,
|
||||
...props
|
||||
}: ImagePropsWithOptionalAlt & { shape?: 0 | 1 | 2 }) {
|
||||
let id = useId()
|
||||
let { width, height, path } = shapes[shape]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
className,
|
||||
'relative flex aspect-719/680 w-full grayscale',
|
||||
)}
|
||||
>
|
||||
<svg viewBox={`0 0 ${width} ${height}`} fill="none" className="h-full">
|
||||
<g clipPath={`url(#${id}-clip)`} className="group">
|
||||
<g className="origin-center scale-100 transition duration-500 motion-safe:group-hover:scale-105">
|
||||
<foreignObject width={width} height={height}>
|
||||
<Image
|
||||
alt=""
|
||||
className="w-full bg-neutral-100 object-cover"
|
||||
style={{ aspectRatio: `${width} / ${height}` }}
|
||||
{...props}
|
||||
/>
|
||||
</foreignObject>
|
||||
</g>
|
||||
<use
|
||||
href={`#${id}-shape`}
|
||||
strokeWidth="2"
|
||||
className="stroke-neutral-950/10"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id={`${id}-clip`}>
|
||||
<path
|
||||
id={`${id}-shape`}
|
||||
d={path}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
34
src/components/TagList.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
export function TagList({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<ul role="list" className={clsx(className, 'flex flex-wrap gap-4')}>
|
||||
{children}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export function TagListItem({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<li
|
||||
className={clsx(
|
||||
'rounded-full bg-neutral-100 px-4 py-1.5 text-base text-neutral-600',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
}
|
44
src/components/Testimonial.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import Image, { type ImageProps } from 'next/image'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Container } from '@/components/Container'
|
||||
import { FadeIn } from '@/components/FadeIn'
|
||||
import { GridPattern } from '@/components/GridPattern'
|
||||
|
||||
export function Testimonial({
|
||||
children,
|
||||
client,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
client: { logo: ImageProps['src']; name: string }
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'relative isolate bg-neutral-50 py-16 sm:py-28 md:py-32',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<GridPattern
|
||||
className="absolute inset-0 -z-10 h-full w-full mask-[linear-gradient(to_bottom_left,white_50%,transparent_60%)] fill-neutral-100 stroke-neutral-950/5"
|
||||
yOffset={-256}
|
||||
/>
|
||||
<Container>
|
||||
<FadeIn>
|
||||
<figure className="mx-auto max-w-4xl">
|
||||
<blockquote className="relative font-display text-3xl font-medium tracking-tight text-neutral-950 sm:text-4xl">
|
||||
<p className="before:content-['“'] after:content-['”'] sm:before:absolute sm:before:right-full">
|
||||
{children}
|
||||
</p>
|
||||
</blockquote>
|
||||
<figcaption className="mt-10">
|
||||
<Image src={client.logo} alt={client.name} unoptimized />
|
||||
</figcaption>
|
||||
</figure>
|
||||
</FadeIn>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
BIN
src/fonts/Mona-Sans.var.woff2
Normal file
9
src/images/clients/bright-path/logo-dark.svg
Normal file
After Width: | Height: | Size: 7.3 KiB |
9
src/images/clients/bright-path/logo-light.svg
Normal file
After Width: | Height: | Size: 7.3 KiB |
6
src/images/clients/bright-path/logomark-dark.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="3" width="30" height="30" rx="12" fill="#0A0A0A" />
|
||||
<path
|
||||
d="M11.5 10H16.5C18.7091 10 20.5 11.7909 20.5 14C20.5 14.805 20.4336 15.5545 20.2282 16.1821C19.9 17.1849 18.7594 17.5 17.7041 17.5H16.5C15.6716 17.5 15 16.8284 15 16C15 15.7239 14.7761 15.5 14.5 15.5C14.2239 15.5 14 15.7239 14 16C14 16.8284 13.3284 17.5 12.5 17.5C12.2239 17.5 12 17.7239 12 18C12 18.2761 12.2239 18.5 12.5 18.5C13.3284 18.5 14 19.1716 14 20C14 20.2761 14.2239 20.5 14.5 20.5C14.7761 20.5 15 20.2761 15 20C15 19.1716 15.6716 18.5 16.5 18.5H17.7041C18.7594 18.5 19.9 18.8151 20.2282 19.8179C20.4336 20.4455 20.5 21.195 20.5 22C20.5 24.2091 18.7091 26 16.5 26H11.5C10.3954 26 9.5 25.1046 9.5 24V12C9.5 10.8954 10.3954 10 11.5 10Z"
|
||||
fill="white" />
|
||||
</svg>
|
After Width: | Height: | Size: 844 B |
6
src/images/clients/bright-path/logomark-light.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="3" width="30" height="30" rx="12" fill="white" />
|
||||
<path
|
||||
d="M11.5 10H16.5C18.7091 10 20.5 11.7909 20.5 14C20.5 14.805 20.4336 15.5545 20.2282 16.1821C19.9 17.1849 18.7594 17.5 17.7041 17.5H16.5C15.6716 17.5 15 16.8284 15 16C15 15.7239 14.7761 15.5 14.5 15.5C14.2239 15.5 14 15.7239 14 16C14 16.8284 13.3284 17.5 12.5 17.5C12.2239 17.5 12 17.7239 12 18C12 18.2761 12.2239 18.5 12.5 18.5C13.3284 18.5 14 19.1716 14 20C14 20.2761 14.2239 20.5 14.5 20.5C14.7761 20.5 15 20.2761 15 20C15 19.1716 15.6716 18.5 16.5 18.5H17.7041C18.7594 18.5 19.9 18.8151 20.2282 19.8179C20.4336 20.4455 20.5 21.195 20.5 22C20.5 24.2091 18.7091 26 16.5 26H11.5C10.3954 26 9.5 25.1046 9.5 24V12C9.5 10.8954 10.3954 10 11.5 10Z"
|
||||
fill="#0A0A0A" />
|
||||
</svg>
|
After Width: | Height: | Size: 844 B |
9
src/images/clients/family-fund/logo-dark.svg
Normal file
After Width: | Height: | Size: 6.6 KiB |
9
src/images/clients/family-fund/logo-light.svg
Normal file
After Width: | Height: | Size: 6.6 KiB |
6
src/images/clients/family-fund/logomark-dark.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="18" r="15" fill="#0A0A0A" />
|
||||
<path d="M8.5 10H19.5L21.5 12L19.5 14H8.5V10Z" fill="white" />
|
||||
<path d="M8.5 16H15.5L17.5 18L15.5 20H8.5V16Z" fill="white" />
|
||||
<rect x="8.5" y="22" width="4" height="4" fill="white" />
|
||||
</svg>
|
After Width: | Height: | Size: 344 B |
6
src/images/clients/family-fund/logomark-light.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="18" r="15" fill="white" />
|
||||
<path d="M8.5 10H19.5L21.5 12L19.5 14H8.5V10Z" fill="#0A0A0A" />
|
||||
<path d="M8.5 16H15.5L17.5 18L15.5 20H8.5V16Z" fill="#0A0A0A" />
|
||||
<rect x="8.5" y="22" width="4" height="4" fill="#0A0A0A" />
|
||||
</svg>
|
After Width: | Height: | Size: 348 B |
11
src/images/clients/green-life/logo-dark.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="184" height="36" viewBox="0 0 184 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="18" r="15" fill="#0A0A0A" />
|
||||
<path
|
||||
d="M15 15.3636C15 11.8491 12.0899 9 8.5 9V16.6364C8.5 20.1509 11.4101 23 15 23C18.5898 23 21.5 20.1509 21.5 16.6364V9C17.9101 9 15 11.8491 15 15.3636Z"
|
||||
fill="white" />
|
||||
<path d="M19 25V25C16.7909 25 15 26.7909 15 29V29V29C17.2091 29 19 27.2091 19 25V25Z" fill="white" />
|
||||
<path d="M11 25V25C13.2091 25 15 26.7909 15 29V29V29C12.7909 29 11 27.2091 11 25V25Z" fill="white" />
|
||||
<path
|
||||
d="M48.973 25.285C52.944 25.285 55.338 22.5015 55.338 18.6255C55.338 18.5495 55.319 18.027 55.3 17.8275H50.455V19.623H52.868C52.5165 21.979 51.177 23.157 49.1155 23.1285C46.3605 23.0905 44.983 21.1525 44.9925 18.16C45.002 15.1675 46.3605 13.163 49.1155 13.201C50.93 13.2105 52.1175 14.2555 52.64 15.8705L54.977 15.5C54.2835 12.688 52.336 11.035 48.973 11.035C45.3155 11.035 42.57 13.676 42.57 18.16C42.57 22.72 45.4105 25.285 48.973 25.285ZM59.165 25V19.7845C59.165 18.597 59.583 17.6375 60.5235 17.1245C61.16 16.754 62.0245 16.7065 62.642 16.8775V14.74C61.7205 14.5975 60.6755 14.759 59.9155 15.2815C59.488 15.5475 59.146 15.937 58.8895 16.3835V14.74H56.866V25H59.165ZM68.5538 25.285C70.5298 25.285 72.2968 24.221 73.0853 22.359L70.8243 21.675C70.3778 22.625 69.5418 23.138 68.4208 23.138C66.8628 23.138 65.9508 22.1975 65.7703 20.5065H73.1993C73.4653 16.811 71.6033 14.455 68.4208 14.455C65.3998 14.455 63.3193 16.621 63.3193 19.984C63.3193 23.1 65.4378 25.285 68.5538 25.285ZM68.5158 16.4595C69.9313 16.4595 70.6818 17.172 70.8813 18.7775H65.8273C66.0933 17.2575 66.9768 16.4595 68.5158 16.4595ZM79.6035 25.285C81.5795 25.285 83.3465 24.221 84.135 22.359L81.874 21.675C81.4275 22.625 80.5915 23.138 79.4705 23.138C77.9125 23.138 77.0005 22.1975 76.82 20.5065H84.249C84.515 16.811 82.653 14.455 79.4705 14.455C76.4495 14.455 74.369 16.621 74.369 19.984C74.369 23.1 76.4875 25.285 79.6035 25.285ZM79.5655 16.4595C80.981 16.4595 81.7315 17.172 81.931 18.7775H76.877C77.143 17.2575 78.0265 16.4595 79.5655 16.4595ZM88.0094 25V19.7275C88.0094 17.2385 89.2444 16.602 90.4129 16.602C92.5979 16.602 92.8069 18.7965 92.8069 20.06V25H95.1249V19.2715C95.1249 18.0555 94.8684 14.4455 91.0874 14.4455C89.6054 14.4455 88.4844 14.9965 87.7339 15.88V14.74H85.6914V25H88.0094ZM109.169 25V22.853H103.127V11.32H100.838V25H109.169ZM112.878 13.182V11.0825H110.589V13.182H112.878ZM112.878 25V14.74H110.589V25H112.878ZM118.526 25V16.5355H121.006V14.74H118.526V14.0845C118.526 13.3815 118.944 12.897 119.818 12.897H121.006V11.035H119.723C118.973 11.035 117.776 10.9685 116.911 11.9375C116.218 12.6975 116.237 13.7995 116.237 14.5785V14.74H114.584V16.5355H116.237V25H118.526ZM126.438 25.285C128.414 25.285 130.181 24.221 130.969 22.359L128.708 21.675C128.262 22.625 127.426 23.138 126.305 23.138C124.747 23.138 123.835 22.1975 123.654 20.5065H131.083C131.349 16.811 129.487 14.455 126.305 14.455C123.284 14.455 121.203 16.621 121.203 19.984C121.203 23.1 123.322 25.285 126.438 25.285ZM126.4 16.4595C127.815 16.4595 128.566 17.172 128.765 18.7775H123.711C123.977 17.2575 124.861 16.4595 126.4 16.4595Z"
|
||||
fill="#0A0A0A" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
11
src/images/clients/green-life/logo-light.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="184" height="36" viewBox="0 0 184 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="18" r="15" fill="white" />
|
||||
<path
|
||||
d="M15 15.3636C15 11.8491 12.0899 9 8.5 9V16.6364C8.5 20.1509 11.4101 23 15 23C18.5898 23 21.5 20.1509 21.5 16.6364V9C17.9101 9 15 11.8491 15 15.3636Z"
|
||||
fill="#0A0A0A" />
|
||||
<path d="M19 25V25C16.7909 25 15 26.7909 15 29V29V29C17.2091 29 19 27.2091 19 25V25Z" fill="#0A0A0A" />
|
||||
<path d="M11 25V25C13.2091 25 15 26.7909 15 29V29V29C12.7909 29 11 27.2091 11 25V25Z" fill="#0A0A0A" />
|
||||
<path
|
||||
d="M48.973 25.285C52.944 25.285 55.338 22.5015 55.338 18.6255C55.338 18.5495 55.319 18.027 55.3 17.8275H50.455V19.623H52.868C52.5165 21.979 51.177 23.157 49.1155 23.1285C46.3605 23.0905 44.983 21.1525 44.9925 18.16C45.002 15.1675 46.3605 13.163 49.1155 13.201C50.93 13.2105 52.1175 14.2555 52.64 15.8705L54.977 15.5C54.2835 12.688 52.336 11.035 48.973 11.035C45.3155 11.035 42.57 13.676 42.57 18.16C42.57 22.72 45.4105 25.285 48.973 25.285ZM59.165 25V19.7845C59.165 18.597 59.583 17.6375 60.5235 17.1245C61.16 16.754 62.0245 16.7065 62.642 16.8775V14.74C61.7205 14.5975 60.6755 14.759 59.9155 15.2815C59.488 15.5475 59.146 15.937 58.8895 16.3835V14.74H56.866V25H59.165ZM68.5538 25.285C70.5298 25.285 72.2968 24.221 73.0853 22.359L70.8243 21.675C70.3778 22.625 69.5418 23.138 68.4208 23.138C66.8628 23.138 65.9508 22.1975 65.7703 20.5065H73.1993C73.4653 16.811 71.6033 14.455 68.4208 14.455C65.3998 14.455 63.3193 16.621 63.3193 19.984C63.3193 23.1 65.4378 25.285 68.5538 25.285ZM68.5158 16.4595C69.9313 16.4595 70.6818 17.172 70.8813 18.7775H65.8273C66.0933 17.2575 66.9768 16.4595 68.5158 16.4595ZM79.6035 25.285C81.5795 25.285 83.3465 24.221 84.135 22.359L81.874 21.675C81.4275 22.625 80.5915 23.138 79.4705 23.138C77.9125 23.138 77.0005 22.1975 76.82 20.5065H84.249C84.515 16.811 82.653 14.455 79.4705 14.455C76.4495 14.455 74.369 16.621 74.369 19.984C74.369 23.1 76.4875 25.285 79.6035 25.285ZM79.5655 16.4595C80.981 16.4595 81.7315 17.172 81.931 18.7775H76.877C77.143 17.2575 78.0265 16.4595 79.5655 16.4595ZM88.0094 25V19.7275C88.0094 17.2385 89.2444 16.602 90.4129 16.602C92.5979 16.602 92.8069 18.7965 92.8069 20.06V25H95.1249V19.2715C95.1249 18.0555 94.8684 14.4455 91.0874 14.4455C89.6054 14.4455 88.4844 14.9965 87.7339 15.88V14.74H85.6914V25H88.0094ZM109.169 25V22.853H103.127V11.32H100.838V25H109.169ZM112.878 13.182V11.0825H110.589V13.182H112.878ZM112.878 25V14.74H110.589V25H112.878ZM118.526 25V16.5355H121.006V14.74H118.526V14.0845C118.526 13.3815 118.944 12.897 119.818 12.897H121.006V11.035H119.723C118.973 11.035 117.776 10.9685 116.911 11.9375C116.218 12.6975 116.237 13.7995 116.237 14.5785V14.74H114.584V16.5355H116.237V25H118.526ZM126.438 25.285C128.414 25.285 130.181 24.221 130.969 22.359L128.708 21.675C128.262 22.625 127.426 23.138 126.305 23.138C124.747 23.138 123.835 22.1975 123.654 20.5065H131.083C131.349 16.811 129.487 14.455 126.305 14.455C123.284 14.455 121.203 16.621 121.203 19.984C121.203 23.1 123.322 25.285 126.438 25.285ZM126.4 16.4595C127.815 16.4595 128.566 17.172 128.765 18.7775H123.711C123.977 17.2575 124.861 16.4595 126.4 16.4595Z"
|
||||
fill="white" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
8
src/images/clients/green-life/logomark-dark.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="18" r="15" fill="#0A0A0A" />
|
||||
<path
|
||||
d="M15 15.3636C15 11.8491 12.0899 9 8.5 9V16.6364C8.5 20.1509 11.4101 23 15 23C18.5898 23 21.5 20.1509 21.5 16.6364V9C17.9101 9 15 11.8491 15 15.3636Z"
|
||||
fill="white" />
|
||||
<path d="M19 25V25C16.7909 25 15 26.7909 15 29V29V29C17.2091 29 19 27.2091 19 25V25Z" fill="white" />
|
||||
<path d="M11 25V25C13.2091 25 15 26.7909 15 29V29V29C12.7909 29 11 27.2091 11 25V25Z" fill="white" />
|
||||
</svg>
|
After Width: | Height: | Size: 546 B |
8
src/images/clients/green-life/logomark-light.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="18" r="15" fill="white" />
|
||||
<path
|
||||
d="M15 15.3636C15 11.8491 12.0899 9 8.5 9V16.6364C8.5 20.1509 11.4101 23 15 23C18.5898 23 21.5 20.1509 21.5 16.6364V9C17.9101 9 15 11.8491 15 15.3636Z"
|
||||
fill="#0A0A0A" />
|
||||
<path d="M19 25V25C16.7909 25 15 26.7909 15 29V29V29C17.2091 29 19 27.2091 19 25V25Z" fill="#0A0A0A" />
|
||||
<path d="M11 25V25C13.2091 25 15 26.7909 15 29V29V29C12.7909 29 11 27.2091 11 25V25Z" fill="#0A0A0A" />
|
||||
</svg>
|
After Width: | Height: | Size: 550 B |
11
src/images/clients/home-work/logo-dark.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="184" height="36" viewBox="0 0 184 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.10557 30.7889C1.03614 30.9277 1 31.0808 1 31.2361V31.5C1 32.0523 1.44772 32.5 2 32.5H28C28.5523 32.5 29 32.0523 29 31.5V31.2361C29 31.0808 28.9639 30.9277 28.8944 30.7889L27.2764 27.5528C27.107 27.214 26.7607 27 26.382 27H3.61803C3.23926 27 2.893 27.214 2.72361 27.5528L1.10557 30.7889Z"
|
||||
fill="#0A0A0A" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M4 12.9739C4 12.6739 4.13463 12.3899 4.36676 12.1999L15 3.5L18.3817 6.26687C19.0287 6.79619 20 6.3359 20 5.5H24V10.3898C24 10.6897 24.1346 10.9738 24.3668 11.1637L25.6332 12.1999C25.8654 12.3899 26 12.6739 26 12.9739V24C26 24.5523 25.5523 25 25 25H5C4.44772 25 4 24.5523 4 24V12.9739ZM15 16C13.3431 16 12 17.3431 12 19V24H18V19C18 17.3431 16.6569 16 15 16Z"
|
||||
fill="#0A0A0A" />
|
||||
<path
|
||||
d="M45.6195 25V19.224H52.0985V25H54.3785V11.32H52.0985V17.077H45.6195V11.32H43.33V25H45.6195ZM61.1934 25.285C64.2619 25.285 66.3139 23.0715 66.3139 19.87C66.3139 16.697 64.2904 14.455 61.1934 14.455C58.1629 14.455 56.0919 16.6495 56.0919 19.87C56.0919 23.043 58.1059 25.285 61.1934 25.285ZM61.1934 23.138C59.4264 23.138 58.5144 21.865 58.5144 19.87C58.5144 17.932 59.3504 16.602 61.1934 16.602C62.9889 16.602 63.8914 17.8845 63.8914 19.87C63.8914 21.7985 63.0079 23.138 61.1934 23.138ZM70.3165 25V18.6825C70.3165 17.4 71.0765 16.526 72.245 16.526C73.442 16.526 74.1925 17.3715 74.1925 18.7965V25H76.4725V18.6825C76.4725 17.324 77.3085 16.526 78.4105 16.526C79.5885 16.526 80.3485 17.362 80.3485 18.768V25H82.638V18.1885C82.638 15.9655 81.3935 14.4835 79.123 14.4835C77.793 14.4835 76.634 15.101 75.9975 16.1175C75.456 15.12 74.449 14.4835 73.005 14.4835C71.77 14.4835 70.706 15.006 70.0315 15.8515V14.74H68.008V25H70.3165ZM89.2334 25.285C91.2094 25.285 92.9764 24.221 93.7649 22.359L91.5039 21.675C91.0574 22.625 90.2214 23.138 89.1004 23.138C87.5424 23.138 86.6304 22.1975 86.4499 20.5065H93.8789C94.1449 16.811 92.2829 14.455 89.1004 14.455C86.0794 14.455 83.9989 16.621 83.9989 19.984C83.9989 23.1 86.1174 25.285 89.2334 25.285ZM89.1954 16.4595C90.6109 16.4595 91.3614 17.172 91.5609 18.7775H86.5069C86.7729 17.2575 87.6564 16.4595 89.1954 16.4595ZM104.191 25L106.984 15.272L109.777 25H112.038L116.028 11.32H113.625L110.908 21.2L108.181 11.32L105.778 11.339L103.061 21.2L100.334 11.32H97.9308L101.93 25H104.191ZM120.598 25.285C123.667 25.285 125.719 23.0715 125.719 19.87C125.719 16.697 123.695 14.455 120.598 14.455C117.568 14.455 115.497 16.6495 115.497 19.87C115.497 23.043 117.511 25.285 120.598 25.285ZM120.598 23.138C118.831 23.138 117.919 21.865 117.919 19.87C117.919 17.932 118.755 16.602 120.598 16.602C122.394 16.602 123.296 17.8845 123.296 19.87C123.296 21.7985 122.413 23.138 120.598 23.138ZM129.731 25V19.7845C129.731 18.597 130.149 17.6375 131.089 17.1245C131.726 16.754 132.59 16.7065 133.208 16.8775V14.74C132.286 14.5975 131.241 14.759 130.481 15.2815C130.054 15.5475 129.712 15.937 129.455 16.3835V14.74H127.432V25H129.731ZM136.959 25V20.06L140.882 25H143.913L139.59 19.87L143.571 14.74H140.711L136.959 19.68V11.32H134.641L134.622 25H136.959Z"
|
||||
fill="#0A0A0A" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
11
src/images/clients/home-work/logo-light.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="184" height="36" viewBox="0 0 184 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.10557 30.7889C1.03614 30.9277 1 31.0808 1 31.2361V31.5C1 32.0523 1.44772 32.5 2 32.5H28C28.5523 32.5 29 32.0523 29 31.5V31.2361C29 31.0808 28.9639 30.9277 28.8944 30.7889L27.2764 27.5528C27.107 27.214 26.7607 27 26.382 27H3.61803C3.23926 27 2.893 27.214 2.72361 27.5528L1.10557 30.7889Z"
|
||||
fill="white" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M4 12.9739C4 12.6739 4.13463 12.3899 4.36676 12.1999L15 3.5L18.3817 6.26687C19.0287 6.79619 20 6.3359 20 5.5H24V10.3898C24 10.6897 24.1346 10.9738 24.3668 11.1637L25.6332 12.1999C25.8654 12.3899 26 12.6739 26 12.9739V24C26 24.5523 25.5523 25 25 25H5C4.44772 25 4 24.5523 4 24V12.9739ZM15 16C13.3431 16 12 17.3431 12 19V24H18V19C18 17.3431 16.6569 16 15 16Z"
|
||||
fill="white" />
|
||||
<path
|
||||
d="M45.6195 25V19.224H52.0985V25H54.3785V11.32H52.0985V17.077H45.6195V11.32H43.33V25H45.6195ZM61.1934 25.285C64.2619 25.285 66.3139 23.0715 66.3139 19.87C66.3139 16.697 64.2904 14.455 61.1934 14.455C58.1629 14.455 56.0919 16.6495 56.0919 19.87C56.0919 23.043 58.1059 25.285 61.1934 25.285ZM61.1934 23.138C59.4264 23.138 58.5144 21.865 58.5144 19.87C58.5144 17.932 59.3504 16.602 61.1934 16.602C62.9889 16.602 63.8914 17.8845 63.8914 19.87C63.8914 21.7985 63.0079 23.138 61.1934 23.138ZM70.3165 25V18.6825C70.3165 17.4 71.0765 16.526 72.245 16.526C73.442 16.526 74.1925 17.3715 74.1925 18.7965V25H76.4725V18.6825C76.4725 17.324 77.3085 16.526 78.4105 16.526C79.5885 16.526 80.3485 17.362 80.3485 18.768V25H82.638V18.1885C82.638 15.9655 81.3935 14.4835 79.123 14.4835C77.793 14.4835 76.634 15.101 75.9975 16.1175C75.456 15.12 74.449 14.4835 73.005 14.4835C71.77 14.4835 70.706 15.006 70.0315 15.8515V14.74H68.008V25H70.3165ZM89.2334 25.285C91.2094 25.285 92.9764 24.221 93.7649 22.359L91.5039 21.675C91.0574 22.625 90.2214 23.138 89.1004 23.138C87.5424 23.138 86.6304 22.1975 86.4499 20.5065H93.8789C94.1449 16.811 92.2829 14.455 89.1004 14.455C86.0794 14.455 83.9989 16.621 83.9989 19.984C83.9989 23.1 86.1174 25.285 89.2334 25.285ZM89.1954 16.4595C90.6109 16.4595 91.3614 17.172 91.5609 18.7775H86.5069C86.7729 17.2575 87.6564 16.4595 89.1954 16.4595ZM104.191 25L106.984 15.272L109.777 25H112.038L116.028 11.32H113.625L110.908 21.2L108.181 11.32L105.778 11.339L103.061 21.2L100.334 11.32H97.9308L101.93 25H104.191ZM120.598 25.285C123.667 25.285 125.719 23.0715 125.719 19.87C125.719 16.697 123.695 14.455 120.598 14.455C117.568 14.455 115.497 16.6495 115.497 19.87C115.497 23.043 117.511 25.285 120.598 25.285ZM120.598 23.138C118.831 23.138 117.919 21.865 117.919 19.87C117.919 17.932 118.755 16.602 120.598 16.602C122.394 16.602 123.296 17.8845 123.296 19.87C123.296 21.7985 122.413 23.138 120.598 23.138ZM129.731 25V19.7845C129.731 18.597 130.149 17.6375 131.089 17.1245C131.726 16.754 132.59 16.7065 133.208 16.8775V14.74C132.286 14.5975 131.241 14.759 130.481 15.2815C130.054 15.5475 129.712 15.937 129.455 16.3835V14.74H127.432V25H129.731ZM136.959 25V20.06L140.882 25H143.913L139.59 19.87L143.571 14.74H140.711L136.959 19.68V11.32H134.641L134.622 25H136.959Z"
|
||||
fill="white" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
8
src/images/clients/home-work/logomark-dark.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.10557 30.7889C1.03614 30.9277 1 31.0808 1 31.2361V31.5C1 32.0523 1.44772 32.5 2 32.5H28C28.5523 32.5 29 32.0523 29 31.5V31.2361C29 31.0808 28.9639 30.9277 28.8944 30.7889L27.2764 27.5528C27.107 27.214 26.7607 27 26.382 27H3.61803C3.23926 27 2.893 27.214 2.72361 27.5528L1.10557 30.7889Z"
|
||||
fill="#0A0A0A" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M4 12.9739C4 12.6739 4.13463 12.3899 4.36676 12.1999L15 3.5L18.3817 6.26687C19.0287 6.79619 20 6.3359 20 5.5H24V10.3898C24 10.6897 24.1346 10.9738 24.3668 11.1637L25.6332 12.1999C25.8654 12.3899 26 12.6739 26 12.9739V24C26 24.5523 25.5523 25 25 25H5C4.44772 25 4 24.5523 4 24V12.9739ZM15 16C13.3431 16 12 17.3431 12 19V24H18V19C18 17.3431 16.6569 16 15 16Z"
|
||||
fill="#0A0A0A" />
|
||||
</svg>
|
After Width: | Height: | Size: 866 B |
8
src/images/clients/home-work/logomark-light.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.10557 30.7889C1.03614 30.9277 1 31.0808 1 31.2361V31.5C1 32.0523 1.44772 32.5 2 32.5H28C28.5523 32.5 29 32.0523 29 31.5V31.2361C29 31.0808 28.9639 30.9277 28.8944 30.7889L27.2764 27.5528C27.107 27.214 26.7607 27 26.382 27H3.61803C3.23926 27 2.893 27.214 2.72361 27.5528L1.10557 30.7889Z"
|
||||
fill="white" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M4 12.9739C4 12.6739 4.13463 12.3899 4.36676 12.1999L15 3.5L18.3817 6.26687C19.0287 6.79619 20 6.3359 20 5.5H24V10.3898C24 10.6897 24.1346 10.9738 24.3668 11.1637L25.6332 12.1999C25.8654 12.3899 26 12.6739 26 12.9739V24C26 24.5523 25.5523 25 25 25H5C4.44772 25 4 24.5523 4 24V12.9739ZM15 16C13.3431 16 12 17.3431 12 19V24H18V19C18 17.3431 16.6569 16 15 16Z"
|
||||
fill="white" />
|
||||
</svg>
|
After Width: | Height: | Size: 862 B |
11
src/images/clients/mail-smirk/logo-dark.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="184" height="36" viewBox="0 0 184 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="18" r="15" fill="#0A0A0A" />
|
||||
<path
|
||||
d="M7 15.154C7 14.7567 7.23519 14.3971 7.59918 14.2379L15 11L22.4008 14.2379C22.7648 14.3971 23 14.7567 23 15.154V21.3944C23 22.3975 22.4943 23.3355 21.5706 23.7267C20.2201 24.2988 17.9607 25 15 25C12.0393 25 9.77993 24.2988 8.42936 23.7267C7.50573 23.3355 7 22.3975 7 21.3944V15.154Z"
|
||||
fill="white" />
|
||||
<path d="M11 18C11 18 12 20.5 15 20.5C18 20.5 19 18 19 18" stroke="#0A0A0A" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
d="M43.6 24H44.928V14.976H44.992L48.96 22.08H49.12L53.088 15.024H53.152V24H54.48V12.8H52.864L49.088 19.632H49.024L45.248 12.8H43.6V24ZM63.8221 12.8H62.1101L57.9981 23.84V24H59.4541L60.5101 21.072H65.4061L66.4621 24H67.9501V23.84L63.8221 12.8ZM60.9581 19.824L62.9261 14.352H62.9901L64.9581 19.824H60.9581ZM71.4644 24H72.8724V12.8H71.4644V24ZM77.5137 24H84.3138V22.72H78.9057V12.8H77.5137V24ZM91.1906 24.144C94.5506 24.144 95.8626 22.704 95.8626 20.704C95.8626 18.512 94.2786 17.664 92.1986 17.232L90.6786 16.912C89.6386 16.688 89.4146 16.304 89.4146 15.872C89.4146 15.408 89.8626 14.896 91.1746 14.896C92.5026 14.896 93.0626 15.568 93.1266 16.304H95.6706C95.6546 14.16 93.7666 12.656 91.2066 12.656C88.5666 12.656 86.8386 13.984 86.8386 15.872C86.8386 18.016 88.3586 18.816 90.2946 19.232L91.9266 19.584C92.9186 19.792 93.2866 20.16 93.2866 20.768C93.2866 21.44 92.7266 21.92 91.1906 21.92C89.9746 21.92 89.1266 21.376 89.0146 20.16H86.4706C86.6146 22.576 88.1986 24.144 91.1906 24.144ZM98.82 24H101.252V16.72H101.316L104.66 22.144H104.82L108.164 16.768H108.228V24H110.66V12.8H107.94L104.788 17.936H104.724L101.572 12.8H98.82V24ZM114.338 24H116.866V12.8H114.338V24ZM127.28 19.744C128.64 19.2 129.488 17.984 129.488 16.368C129.488 14.464 128.048 12.8 125.664 12.8H120.544V24H123.056V20.048H124.624L127.04 24H129.984V23.84L127.28 19.744ZM123.056 15.04H125.536C126.512 15.04 126.912 15.712 126.912 16.384C126.912 17.088 126.544 17.808 125.536 17.808H123.056V15.04ZM137.231 18.176L142.047 12.96V12.8H139.103L135.215 17.04V12.8H132.703V24H135.215V19.36L139.263 24H142.239V23.84L137.231 18.176Z"
|
||||
fill="#0A0A0A" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
11
src/images/clients/mail-smirk/logo-light.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="184" height="36" viewBox="0 0 184 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="18" r="15" fill="white" />
|
||||
<path
|
||||
d="M7 15.154C7 14.7567 7.23519 14.3971 7.59918 14.2379L15 11L22.4008 14.2379C22.7648 14.3971 23 14.7567 23 15.154V21.3944C23 22.3975 22.4943 23.3355 21.5706 23.7267C20.2201 24.2988 17.9607 25 15 25C12.0393 25 9.77993 24.2988 8.42936 23.7267C7.50573 23.3355 7 22.3975 7 21.3944V15.154Z"
|
||||
fill="#0A0A0A" />
|
||||
<path d="M11 18C11 18 12 20.5 15 20.5C18 20.5 19 18 19 18" stroke="white" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
d="M43.6 24H44.928V14.976H44.992L48.96 22.08H49.12L53.088 15.024H53.152V24H54.48V12.8H52.864L49.088 19.632H49.024L45.248 12.8H43.6V24ZM63.8221 12.8H62.1101L57.9981 23.84V24H59.4541L60.5101 21.072H65.4061L66.4621 24H67.9501V23.84L63.8221 12.8ZM60.9581 19.824L62.9261 14.352H62.9901L64.9581 19.824H60.9581ZM71.4644 24H72.8724V12.8H71.4644V24ZM77.5137 24H84.3138V22.72H78.9057V12.8H77.5137V24ZM91.1906 24.144C94.5506 24.144 95.8626 22.704 95.8626 20.704C95.8626 18.512 94.2786 17.664 92.1986 17.232L90.6786 16.912C89.6386 16.688 89.4146 16.304 89.4146 15.872C89.4146 15.408 89.8626 14.896 91.1746 14.896C92.5026 14.896 93.0626 15.568 93.1266 16.304H95.6706C95.6546 14.16 93.7666 12.656 91.2066 12.656C88.5666 12.656 86.8386 13.984 86.8386 15.872C86.8386 18.016 88.3586 18.816 90.2946 19.232L91.9266 19.584C92.9186 19.792 93.2866 20.16 93.2866 20.768C93.2866 21.44 92.7266 21.92 91.1906 21.92C89.9746 21.92 89.1266 21.376 89.0146 20.16H86.4706C86.6146 22.576 88.1986 24.144 91.1906 24.144ZM98.82 24H101.252V16.72H101.316L104.66 22.144H104.82L108.164 16.768H108.228V24H110.66V12.8H107.94L104.788 17.936H104.724L101.572 12.8H98.82V24ZM114.338 24H116.866V12.8H114.338V24ZM127.28 19.744C128.64 19.2 129.488 17.984 129.488 16.368C129.488 14.464 128.048 12.8 125.664 12.8H120.544V24H123.056V20.048H124.624L127.04 24H129.984V23.84L127.28 19.744ZM123.056 15.04H125.536C126.512 15.04 126.912 15.712 126.912 16.384C126.912 17.088 126.544 17.808 125.536 17.808H123.056V15.04ZM137.231 18.176L142.047 12.96V12.8H139.103L135.215 17.04V12.8H132.703V24H135.215V19.36L139.263 24H142.239V23.84L137.231 18.176Z"
|
||||
fill="white" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
8
src/images/clients/mail-smirk/logomark-dark.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="18" r="15" fill="#0A0A0A" />
|
||||
<path
|
||||
d="M7 15.154C7 14.7567 7.23519 14.3971 7.59918 14.2379L15 11L22.4008 14.2379C22.7648 14.3971 23 14.7567 23 15.154V21.3944C23 22.3975 22.4943 23.3355 21.5706 23.7267C20.2201 24.2988 17.9607 25 15 25C12.0393 25 9.77993 24.2988 8.42936 23.7267C7.50573 23.3355 7 22.3975 7 21.3944V15.154Z"
|
||||
fill="white" />
|
||||
<path d="M11 18C11 18 12 20.5 15 20.5C18 20.5 19 18 19 18" stroke="#0A0A0A" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
After Width: | Height: | Size: 604 B |
8
src/images/clients/mail-smirk/logomark-light.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15" cy="18" r="15" fill="white" />
|
||||
<path
|
||||
d="M7 15.154C7 14.7567 7.23519 14.3971 7.59918 14.2379L15 11L22.4008 14.2379C22.7648 14.3971 23 14.7567 23 15.154V21.3944C23 22.3975 22.4943 23.3355 21.5706 23.7267C20.2201 24.2988 17.9607 25 15 25C12.0393 25 9.77993 24.2988 8.42936 23.7267C7.50573 23.3355 7 22.3975 7 21.3944V15.154Z"
|
||||
fill="#0A0A0A" />
|
||||
<path d="M11 18C11 18 12 20.5 15 20.5C18 20.5 19 18 19 18" stroke="white" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
After Width: | Height: | Size: 602 B |
9
src/images/clients/north-adventures/logo-dark.svg
Normal file
After Width: | Height: | Size: 9.9 KiB |
9
src/images/clients/north-adventures/logo-light.svg
Normal file
After Width: | Height: | Size: 9.9 KiB |
6
src/images/clients/north-adventures/logomark-dark.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="3" width="30" height="30" rx="8" fill="#0A0A0A" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M16.8978 24.5245C18.0241 24.1337 18.9697 23.4994 19.7122 22.7122C20.4994 21.9698 21.1337 21.0241 21.5245 19.8978L24.5 8.5L13.1022 11.4755C11.9759 11.8663 11.0302 12.5006 10.2878 13.2878C9.50061 14.0302 8.8663 14.9759 8.47549 16.1022L5.5 27.5L16.8978 24.5245ZM18 15.5C18 14.9477 17.5523 14.5 17 14.5C16.4477 14.5 16 14.9477 16 15.5V17.6492L14.6713 15.9883C13.7855 14.8811 12 15.5075 12 16.9254V20.5C12 21.0523 12.4477 21.5 13 21.5C13.5523 21.5 14 21.0523 14 20.5V18.3508L15.3287 20.0117C16.2145 21.1189 18 20.4925 18 19.0746V15.5Z"
|
||||
fill="white" />
|
||||
</svg>
|
After Width: | Height: | Size: 771 B |
6
src/images/clients/north-adventures/logomark-light.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="3" width="30" height="30" rx="8" fill="white" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M16.8978 24.5245C18.0241 24.1337 18.9697 23.4994 19.7122 22.7122C20.4994 21.9698 21.1337 21.0241 21.5245 19.8978L24.5 8.5L13.1022 11.4755C11.9759 11.8663 11.0302 12.5006 10.2878 13.2878C9.50061 14.0302 8.8663 14.9759 8.47549 16.1022L5.5 27.5L16.8978 24.5245ZM18 15.5C18 14.9477 17.5523 14.5 17 14.5C16.4477 14.5 16 14.9477 16 15.5V17.6492L14.6713 15.9883C13.7855 14.8811 12 15.5075 12 16.9254V20.5C12 21.0523 12.4477 21.5 13 21.5C13.5523 21.5 14 21.0523 14 20.5V18.3508L15.3287 20.0117C16.2145 21.1189 18 20.4925 18 19.0746V15.5Z"
|
||||
fill="#0A0A0A" />
|
||||
</svg>
|
After Width: | Height: | Size: 771 B |
12
src/images/clients/phobia/logo-dark.svg
Normal file
After Width: | Height: | Size: 11 KiB |
12
src/images/clients/phobia/logo-light.svg
Normal file
After Width: | Height: | Size: 11 KiB |
9
src/images/clients/phobia/logomark-dark.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M2.88146 6.03314C6.38837 3.0447 11.9315 3.58464 15 7C18.0685 3.58464 23.6116 3.0447 27.1185 6.03314C30.6254 9.02158 30.9808 14.2129 27.9123 17.6282L16.4877 30.344C15.6931 31.2285 14.3069 31.2285 13.5123 30.344L2.08773 17.6282C-0.98081 14.2129 -0.625447 9.02158 2.88146 6.03314Z"
|
||||
fill="#0A0A0A" />
|
||||
<path d="M11 17V17.5" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M19 17V17.5" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 23C12 23 12.75 22 15 22C17.25 22 18 23 18 23" stroke="white" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
After Width: | Height: | Size: 801 B |
9
src/images/clients/phobia/logomark-light.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M2.88146 6.03314C6.38837 3.0447 11.9315 3.58464 15 7C18.0685 3.58464 23.6116 3.0447 27.1185 6.03314C30.6254 9.02158 30.9808 14.2129 27.9123 17.6282L16.4877 30.344C15.6931 31.2285 14.3069 31.2285 13.5123 30.344L2.08773 17.6282C-0.98081 14.2129 -0.625447 9.02158 2.88146 6.03314Z"
|
||||
fill="white" />
|
||||
<path d="M11 17V17.5" stroke="#0A0A0A" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M19 17V17.5" stroke="#0A0A0A" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 23C12 23 12.75 22 15 22C17.25 22 18 23 18 23" stroke="#0A0A0A" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
After Width: | Height: | Size: 805 B |
9
src/images/clients/unseal/logo-dark.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="184" height="36" viewBox="0 0 184 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 20C1 26.6274 6.37258 32 13 32H29V4H1V20Z" fill="#0A0A0A" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.5 10.5V21.5L22.5 21.5V25.5H11.5C9.29086 25.5 7.5 23.7091 7.5 21.5V10.5H11.5ZM22.5 10.5H18.5V15.5C18.5 16.6046 19.3954 17.5 20.5 17.5C21.6046 17.5 22.5 16.6046 22.5 15.5V10.5Z"
|
||||
fill="white" />
|
||||
<path
|
||||
d="M43.5791 10.252H46.7588V18.7812C46.7588 20.1348 46.7982 21.012 46.877 21.4131C47.013 22.0576 47.3353 22.5768 47.8438 22.9707C48.3594 23.3574 49.0612 23.5508 49.9492 23.5508C50.8516 23.5508 51.5319 23.3682 51.9902 23.0029C52.4486 22.6305 52.7243 22.1758 52.8174 21.6387C52.9105 21.1016 52.957 20.21 52.957 18.9639V10.252H56.1367V18.5234C56.1367 20.4141 56.0508 21.7497 55.8789 22.5303C55.707 23.3109 55.3883 23.9697 54.9229 24.5068C54.4645 25.0439 53.8486 25.4736 53.0752 25.7959C52.3018 26.111 51.292 26.2686 50.0459 26.2686C48.542 26.2686 47.3997 26.0967 46.6191 25.7529C45.8457 25.402 45.2334 24.9508 44.7822 24.3994C44.3311 23.8408 44.0339 23.2572 43.8906 22.6484C43.6829 21.7461 43.5791 20.4141 43.5791 18.6523V10.252ZM69.1945 26H66.1759V20.1777C66.1759 18.946 66.1115 18.151 65.9826 17.793C65.8537 17.4277 65.6424 17.1449 65.3488 16.9443C65.0623 16.7438 64.715 16.6436 64.3068 16.6436C63.784 16.6436 63.3149 16.7868 62.8996 17.0732C62.4842 17.3597 62.1977 17.7393 62.0402 18.2119C61.8898 18.6846 61.8146 19.5583 61.8146 20.833V26H58.7961V14.5918H61.5998V16.2676C62.5952 14.9785 63.8485 14.334 65.3595 14.334C66.0255 14.334 66.6343 14.4557 67.1857 14.6992C67.7371 14.9355 68.1525 15.2399 68.4318 15.6123C68.7183 15.9847 68.9152 16.4072 69.0226 16.8799C69.1372 17.3525 69.1945 18.0293 69.1945 18.9102V26ZM70.5433 22.7451L73.5726 22.2832C73.7015 22.8704 73.9629 23.318 74.3568 23.626C74.7506 23.9268 75.3021 24.0771 76.0111 24.0771C76.7917 24.0771 77.3789 23.9339 77.7728 23.6475C78.0377 23.4469 78.1702 23.1784 78.1702 22.8418C78.1702 22.6126 78.0986 22.4229 77.9554 22.2725C77.805 22.1292 77.4684 21.9967 76.9456 21.875C74.5107 21.3379 72.9674 20.8473 72.3157 20.4033C71.4134 19.7874 70.9622 18.9316 70.9622 17.8359C70.9622 16.8477 71.3525 16.0169 72.1331 15.3438C72.9137 14.6706 74.124 14.334 75.764 14.334C77.3252 14.334 78.4853 14.5882 79.2445 15.0967C80.0036 15.6051 80.5264 16.3571 80.8128 17.3525L77.9661 17.8789C77.8444 17.4349 77.6116 17.0947 77.2679 16.8584C76.9313 16.6221 76.4479 16.5039 75.8177 16.5039C75.0228 16.5039 74.4534 16.6149 74.1097 16.8369C73.8805 16.9945 73.7659 17.1986 73.7659 17.4492C73.7659 17.6641 73.8662 17.8467 74.0667 17.9971C74.3389 18.1976 75.277 18.4805 76.8812 18.8457C78.4925 19.2109 79.6168 19.6585 80.2542 20.1885C80.8844 20.7256 81.1995 21.474 81.1995 22.4336C81.1995 23.4792 80.7627 24.3779 79.889 25.1299C79.0153 25.8818 77.7226 26.2578 76.0111 26.2578C74.457 26.2578 73.2252 25.9427 72.3157 25.3125C71.4134 24.6823 70.8226 23.8265 70.5433 22.7451ZM89.7993 22.3691L92.8071 22.874C92.4204 23.9769 91.8081 24.8184 90.9702 25.3984C90.1395 25.9714 89.0975 26.2578 87.8442 26.2578C85.8605 26.2578 84.3924 25.6097 83.4399 24.3135C82.688 23.2751 82.312 21.9645 82.312 20.3818C82.312 18.4912 82.8061 17.0124 83.7944 15.9453C84.7827 14.8711 86.0324 14.334 87.5434 14.334C89.2407 14.334 90.5799 14.8962 91.561 16.0205C92.5421 17.1377 93.0112 18.8529 92.9682 21.166H85.4057C85.4272 22.0612 85.6707 22.7594 86.1362 23.2607C86.6017 23.7549 87.1818 24.002 87.8764 24.002C88.3491 24.002 88.7466 23.873 89.0688 23.6152C89.3911 23.3574 89.6346 22.9421 89.7993 22.3691ZM89.9712 19.3184C89.9497 18.4447 89.7241 17.7822 89.2944 17.3311C88.8647 16.8727 88.3419 16.6436 87.7261 16.6436C87.0672 16.6436 86.5229 16.8835 86.0932 17.3633C85.6636 17.8431 85.4523 18.4948 85.4595 19.3184H89.9712ZM97.0348 18.0723L94.2955 17.5781C94.6035 16.4753 95.1334 15.6589 95.8854 15.1289C96.6373 14.599 97.7545 14.334 99.237 14.334C100.583 14.334 101.586 14.4951 102.245 14.8174C102.904 15.1325 103.366 15.5371 103.631 16.0312C103.903 16.5182 104.039 17.417 104.039 18.7275L104.006 22.251C104.006 23.2536 104.053 23.9948 104.146 24.4746C104.246 24.9473 104.429 25.4557 104.694 26H101.708C101.629 25.7995 101.532 25.5023 101.418 25.1084C101.367 24.9294 101.332 24.8112 101.31 24.7539C100.795 25.2552 100.243 25.6312 99.6559 25.8818C99.0687 26.1325 98.442 26.2578 97.776 26.2578C96.6015 26.2578 95.6741 25.9391 94.9938 25.3018C94.3206 24.6644 93.984 23.8587 93.984 22.8848C93.984 22.2402 94.138 21.6673 94.4459 21.166C94.7539 20.6576 95.1836 20.2708 95.735 20.0059C96.2936 19.7337 97.0957 19.4974 98.1413 19.2969C99.5521 19.0319 100.53 18.7848 101.074 18.5557V18.2549C101.074 17.6748 100.931 17.263 100.644 17.0195C100.358 16.7689 99.817 16.6436 99.0221 16.6436C98.485 16.6436 98.0661 16.751 97.7653 16.9658C97.4645 17.1735 97.221 17.5423 97.0348 18.0723ZM101.074 20.5215C100.687 20.6504 100.075 20.8044 99.237 20.9834C98.3991 21.1624 97.8512 21.3379 97.5934 21.5098C97.1995 21.7891 97.0026 22.1436 97.0026 22.5732C97.0026 22.9958 97.1601 23.361 97.4752 23.6689C97.7903 23.9769 98.1914 24.1309 98.6784 24.1309C99.2226 24.1309 99.7418 23.9518 100.236 23.5938C100.601 23.3216 100.841 22.9886 100.956 22.5947C101.034 22.3369 101.074 21.8464 101.074 21.123V20.5215ZM106.365 26V10.252H109.384V26H106.365Z"
|
||||
fill="#0A0A0A" />
|
||||
</svg>
|
After Width: | Height: | Size: 5.2 KiB |
9
src/images/clients/unseal/logo-light.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="184" height="36" viewBox="0 0 184 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 20C1 26.6274 6.37258 32 13 32H29V4H1V20Z" fill="white" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.5 10.5V21.5L22.5 21.5V25.5H11.5C9.29086 25.5 7.5 23.7091 7.5 21.5V10.5H11.5ZM22.5 10.5H18.5V15.5C18.5 16.6046 19.3954 17.5 20.5 17.5C21.6046 17.5 22.5 16.6046 22.5 15.5V10.5Z"
|
||||
fill="#0A0A0A" />
|
||||
<path
|
||||
d="M43.5791 10.252H46.7588V18.7812C46.7588 20.1348 46.7982 21.012 46.877 21.4131C47.013 22.0576 47.3353 22.5768 47.8438 22.9707C48.3594 23.3574 49.0612 23.5508 49.9492 23.5508C50.8516 23.5508 51.5319 23.3682 51.9902 23.0029C52.4486 22.6305 52.7243 22.1758 52.8174 21.6387C52.9105 21.1016 52.957 20.21 52.957 18.9639V10.252H56.1367V18.5234C56.1367 20.4141 56.0508 21.7497 55.8789 22.5303C55.707 23.3109 55.3883 23.9697 54.9229 24.5068C54.4645 25.0439 53.8486 25.4736 53.0752 25.7959C52.3018 26.111 51.292 26.2686 50.0459 26.2686C48.542 26.2686 47.3997 26.0967 46.6191 25.7529C45.8457 25.402 45.2334 24.9508 44.7822 24.3994C44.3311 23.8408 44.0339 23.2572 43.8906 22.6484C43.6829 21.7461 43.5791 20.4141 43.5791 18.6523V10.252ZM69.1945 26H66.1759V20.1777C66.1759 18.946 66.1115 18.151 65.9826 17.793C65.8537 17.4277 65.6424 17.1449 65.3488 16.9443C65.0623 16.7438 64.715 16.6436 64.3068 16.6436C63.784 16.6436 63.3149 16.7868 62.8996 17.0732C62.4842 17.3597 62.1977 17.7393 62.0402 18.2119C61.8898 18.6846 61.8146 19.5583 61.8146 20.833V26H58.7961V14.5918H61.5998V16.2676C62.5952 14.9785 63.8485 14.334 65.3595 14.334C66.0255 14.334 66.6343 14.4557 67.1857 14.6992C67.7371 14.9355 68.1525 15.2399 68.4318 15.6123C68.7183 15.9847 68.9152 16.4072 69.0226 16.8799C69.1372 17.3525 69.1945 18.0293 69.1945 18.9102V26ZM70.5433 22.7451L73.5726 22.2832C73.7015 22.8704 73.9629 23.318 74.3568 23.626C74.7506 23.9268 75.3021 24.0771 76.0111 24.0771C76.7917 24.0771 77.3789 23.9339 77.7728 23.6475C78.0377 23.4469 78.1702 23.1784 78.1702 22.8418C78.1702 22.6126 78.0986 22.4229 77.9554 22.2725C77.805 22.1292 77.4684 21.9967 76.9456 21.875C74.5107 21.3379 72.9674 20.8473 72.3157 20.4033C71.4134 19.7874 70.9622 18.9316 70.9622 17.8359C70.9622 16.8477 71.3525 16.0169 72.1331 15.3438C72.9137 14.6706 74.124 14.334 75.764 14.334C77.3252 14.334 78.4853 14.5882 79.2445 15.0967C80.0036 15.6051 80.5264 16.3571 80.8128 17.3525L77.9661 17.8789C77.8444 17.4349 77.6116 17.0947 77.2679 16.8584C76.9313 16.6221 76.4479 16.5039 75.8177 16.5039C75.0228 16.5039 74.4534 16.6149 74.1097 16.8369C73.8805 16.9945 73.7659 17.1986 73.7659 17.4492C73.7659 17.6641 73.8662 17.8467 74.0667 17.9971C74.3389 18.1976 75.277 18.4805 76.8812 18.8457C78.4925 19.2109 79.6168 19.6585 80.2542 20.1885C80.8844 20.7256 81.1995 21.474 81.1995 22.4336C81.1995 23.4792 80.7627 24.3779 79.889 25.1299C79.0153 25.8818 77.7226 26.2578 76.0111 26.2578C74.457 26.2578 73.2252 25.9427 72.3157 25.3125C71.4134 24.6823 70.8226 23.8265 70.5433 22.7451ZM89.7993 22.3691L92.8071 22.874C92.4204 23.9769 91.8081 24.8184 90.9702 25.3984C90.1395 25.9714 89.0975 26.2578 87.8442 26.2578C85.8605 26.2578 84.3924 25.6097 83.4399 24.3135C82.688 23.2751 82.312 21.9645 82.312 20.3818C82.312 18.4912 82.8061 17.0124 83.7944 15.9453C84.7827 14.8711 86.0324 14.334 87.5434 14.334C89.2407 14.334 90.5799 14.8962 91.561 16.0205C92.5421 17.1377 93.0112 18.8529 92.9682 21.166H85.4057C85.4272 22.0612 85.6707 22.7594 86.1362 23.2607C86.6017 23.7549 87.1818 24.002 87.8764 24.002C88.3491 24.002 88.7466 23.873 89.0688 23.6152C89.3911 23.3574 89.6346 22.9421 89.7993 22.3691ZM89.9712 19.3184C89.9497 18.4447 89.7241 17.7822 89.2944 17.3311C88.8647 16.8727 88.3419 16.6436 87.7261 16.6436C87.0672 16.6436 86.5229 16.8835 86.0932 17.3633C85.6636 17.8431 85.4523 18.4948 85.4595 19.3184H89.9712ZM97.0348 18.0723L94.2955 17.5781C94.6035 16.4753 95.1334 15.6589 95.8854 15.1289C96.6373 14.599 97.7545 14.334 99.237 14.334C100.583 14.334 101.586 14.4951 102.245 14.8174C102.904 15.1325 103.366 15.5371 103.631 16.0312C103.903 16.5182 104.039 17.417 104.039 18.7275L104.006 22.251C104.006 23.2536 104.053 23.9948 104.146 24.4746C104.246 24.9473 104.429 25.4557 104.694 26H101.708C101.629 25.7995 101.532 25.5023 101.418 25.1084C101.367 24.9294 101.332 24.8112 101.31 24.7539C100.795 25.2552 100.243 25.6312 99.6559 25.8818C99.0687 26.1325 98.442 26.2578 97.776 26.2578C96.6015 26.2578 95.6741 25.9391 94.9938 25.3018C94.3206 24.6644 93.984 23.8587 93.984 22.8848C93.984 22.2402 94.138 21.6673 94.4459 21.166C94.7539 20.6576 95.1836 20.2708 95.735 20.0059C96.2936 19.7337 97.0957 19.4974 98.1413 19.2969C99.5521 19.0319 100.53 18.7848 101.074 18.5557V18.2549C101.074 17.6748 100.931 17.263 100.644 17.0195C100.358 16.7689 99.817 16.6436 99.0221 16.6436C98.485 16.6436 98.0661 16.751 97.7653 16.9658C97.4645 17.1735 97.221 17.5423 97.0348 18.0723ZM101.074 20.5215C100.687 20.6504 100.075 20.8044 99.237 20.9834C98.3991 21.1624 97.8512 21.3379 97.5934 21.5098C97.1995 21.7891 97.0026 22.1436 97.0026 22.5732C97.0026 22.9958 97.1601 23.361 97.4752 23.6689C97.7903 23.9769 98.1914 24.1309 98.6784 24.1309C99.2226 24.1309 99.7418 23.9518 100.236 23.5938C100.601 23.3216 100.841 22.9886 100.956 22.5947C101.034 22.3369 101.074 21.8464 101.074 21.123V20.5215ZM106.365 26V10.252H109.384V26H106.365Z"
|
||||
fill="white" />
|
||||
</svg>
|
After Width: | Height: | Size: 5.2 KiB |
6
src/images/clients/unseal/logomark-dark.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 20C1 26.6274 6.37258 32 13 32H29V4H1V20Z" fill="#0A0A0A" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.5 10.5V21.5L22.5 21.5V25.5H11.5C9.29086 25.5 7.5 23.7091 7.5 21.5V10.5H11.5ZM22.5 10.5H18.5V15.5C18.5 16.6046 19.3954 17.5 20.5 17.5C21.6046 17.5 22.5 16.6046 22.5 15.5V10.5Z"
|
||||
fill="white" />
|
||||
</svg>
|
After Width: | Height: | Size: 432 B |
6
src/images/clients/unseal/logomark-light.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 20C1 26.6274 6.37258 32 13 32H29V4H1V20Z" fill="white" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.5 10.5V21.5L22.5 21.5V25.5H11.5C9.29086 25.5 7.5 23.7091 7.5 21.5V10.5H11.5ZM22.5 10.5H18.5V15.5C18.5 16.6046 19.3954 17.5 20.5 17.5C21.6046 17.5 22.5 16.6046 22.5 15.5V10.5Z"
|
||||
fill="#0A0A0A" />
|
||||
</svg>
|
After Width: | Height: | Size: 432 B |
BIN
src/images/laptop.jpg
Normal file
After Width: | Height: | Size: 516 KiB |
BIN
src/images/meeting.jpg
Normal file
After Width: | Height: | Size: 333 KiB |
BIN
src/images/team/angela-fisher.jpg
Normal file
After Width: | Height: | Size: 241 KiB |