Added new page
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 2m39s

This commit is contained in:
Ryan Freeman 2024-10-09 21:37:00 +01:00
parent 5e8da79a82
commit 6c0100e9a3
13 changed files with 184 additions and 5 deletions

View File

@ -4,7 +4,7 @@ import React from 'react';
export const metadata = {
title: 'Reading - Ryan Freeman',
description: 'I have many leather-bound books, take a look at my book recommendations.'
description: 'Take a look at my curated reading list.'
}
type Book = {
@ -69,7 +69,7 @@ export default async function Reading() {
return (
<SimpleLayout
heading="What's on my bookshelf"
heading="Books I'm reading at the moment"
description={metadata.description}
gradient="bg-gradient-to-r from-sky-400 to-blue-500">
<ul

95
app/services/page.tsx Normal file
View File

@ -0,0 +1,95 @@
import {SimpleLayout} from '@/components/layouts/SimpleLayout'
import {Card} from '@/components/ui/Card'
import React, {ElementType} from 'react'
import {CloudIcon} from '@/components/icons/CloudIcon'
import {DatabaseIcon} from '@/components/icons/DatabaseIcon'
import {AppIcon} from '@/components/icons/AppIcon'
import {CodeIcon} from '@/components/icons/CodeIcon'
import {ShieldIcon} from '@/components/icons/ShieldIcon'
import {EmailIcon} from '@/components/icons/EmailIcon'
import {RocketIcon} from '@/components/icons/RocketIcon'
export const metadata = {
title: 'Services - Ryan Freeman',
description: 'Whether you need a WordPress website, React app, AWS support or odd coding jobs, I\'m here to help. As an experienced software engineer, I produce high-quality software that will deliver immediate value for you and your customers.'
}
type Services = {
title: string
description: string
icon: ElementType
}
const iconStyles = `
w-6
h-6
mr-2
z-10
transition
stroke-zinc-500
dark:stroke-zinc-400
group-hover:dark:stroke-indigo-500
group-hover:stroke-indigo-500
`
export default async function Services() {
const services: Services[] = [
{
title: 'AWS',
description: 'As an AWS Certified Cloud Practitioner I can advise on and implement reliable, cost-effective cloud solutions for your business.',
icon: () => <CloudIcon className={iconStyles}/>
},
{
title: 'Databases',
description: 'Not all database technologies are the same, I\'ll help you choose the right database for your use case.',
icon: () => <DatabaseIcon className={iconStyles}/>
},
{
title: 'WordPress',
description: 'WordPress is the de-facto software for building SEO-friendly websites, together we can achieve top rankings in Google search results.',
icon: () => <AppIcon className={iconStyles}/>
},
{
title: 'Frontend',
description: 'Using React, I can deliver modern, responsive websites and applications that seamlessly adapt to any screen size.',
icon: () => <CodeIcon className={iconStyles}/>
},
{
title: 'Backend',
description: 'From building APIs to authentication and integrating third-party services, I develop robust backend systems for your business needs.',
icon: () => <RocketIcon className={iconStyles}/>
},
{
title: 'Domain and hosting',
description: 'Whether youre launching a new website or migrating an existing one, I\'ll ensure your website is fast, secure and always online.',
icon: () => <ShieldIcon className={iconStyles}/>
},
{
title: 'Email',
description: 'I\'ll help you establish trust with your clients by using a custom domain for your email that reflects your brand.',
icon: () => <EmailIcon className={iconStyles}/>
}
]
return (
<SimpleLayout
heading="I offer a wide range of digital services to elevate and transform your business"
description={metadata.description}
gradient="bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400">
<ul
role="list"
className="grid grid-cols-1 gap-x-12 gap-y-16 sm:grid-cols-2 lg:grid-cols-3"
>
{services.map(({title, description, icon: Icon}) => (
<Card as="li" key={title}>
<h2 className="flex items-center text-base font-semibold group-hover:text-indigo-500 text-zinc-800 dark:text-zinc-100">
<Icon/>
<Card.Link href="mailto:hello@ryanfreeman.dev" ariaLabel={title}>{title}</Card.Link>
</h2>
<Card.Description>{description}</Card.Description>
</Card>
))}
</ul>
</SimpleLayout>
)
}

View File

@ -5,6 +5,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const urls = [
'https://ryanfreeman.dev/',
'https://ryanfreeman.dev/about',
'https://ryanfreeman.dev/services',
'https://ryanfreeman.dev/reading',
'https://ryanfreeman.dev/writing',
'https://ryanfreeman.dev/projects',

View File

@ -1,5 +1,5 @@
import React from 'react'
import {OuterContainer, InnerContainer} from './Container'
import {InnerContainer, OuterContainer} from './Container'
import {NavLink} from '@/components/ui/Navigation'
import {SpotifyPlayer} from '@/components/ui/SpotifyPlayer'
import {SocialLink} from '@/components/ui/SocialLink'
@ -12,12 +12,15 @@ export function Footer() {
<OuterContainer>
<div className="border-t border-zinc-100 pt-10 pb-16 dark:border-zinc-700/40">
<InnerContainer>
<SpotifyPlayer/>
{process.env.NODE_ENV !== 'development' &&
<SpotifyPlayer/>
}
<div className="flex flex-col items-center justify-between gap-6 mt-12">
<div
className="flex flex-wrap justify-center gap-6 text-sm font-medium text-zinc-800 dark:text-zinc-200">
<NavLink href="/">Home</NavLink>
<NavLink href="/about">About</NavLink>
<NavLink href="/services">Services</NavLink>
<NavLink href="/reading">Reading</NavLink>
<NavLink href="/writing">Writing</NavLink>
<NavLink href="/projects">Projects</NavLink>

View File

@ -0,0 +1,11 @@
import {Props} from '@/types'
export function AppIcon(props: Props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor"
{...props}>
<path strokeLinecap="round" strokeLinejoin="round"
d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9"/>
</svg>
)
}

View File

@ -0,0 +1,12 @@
import {Props} from '@/types'
export function CloudIcon(props: Props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor"
{...props}>
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 15a4.5 4.5 0 0 0 4.5 4.5H18a3.75 3.75 0 0 0 1.332-7.257 3 3 0 0 0-3.758-3.848 5.25 5.25 0 0 0-10.233 2.33A4.502 4.502 0 0 0 2.25 15Z"/>
</svg>
)
}

View File

@ -0,0 +1,11 @@
import {Props} from '@/types'
export function CodeIcon(props: Props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor"
{...props}>
<path strokeLinecap="round" strokeLinejoin="round"
d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5"/>
</svg>
)
}

View File

@ -0,0 +1,11 @@
import {Props} from '@/types'
export function DatabaseIcon(props: Props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor"
{...props}>
<path strokeLinecap="round" strokeLinejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"/>
</svg>
)
}

View File

@ -0,0 +1,11 @@
import {Props} from '@/types'
export function EmailIcon(props: Props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor"
{...props}>
<path strokeLinecap="round" strokeLinejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"/>
</svg>
)
}

View File

@ -0,0 +1,11 @@
import {Props} from '@/types'
export function RocketIcon(props: Props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor"
{...props}>
<path strokeLinecap="round" strokeLinejoin="round"
d="M15.59 14.37a6 6 0 0 1-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 0 0 6.16-12.12A14.98 14.98 0 0 0 9.631 8.41m5.96 5.96a14.926 14.926 0 0 1-5.841 2.58m-.119-8.54a6 6 0 0 0-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 0 0-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 0 1-2.448-2.448 14.9 14.9 0 0 1 .06-.312m-2.24 2.39a4.493 4.493 0 0 0-1.757 4.306 4.493 4.493 0 0 0 4.306-1.758M16.5 9a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z"/>
</svg>
)
}

View File

@ -0,0 +1,11 @@
import {Props} from '@/types'
export function ShieldIcon(props: Props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor"
{...props}>
<path strokeLinecap="round" strokeLinejoin="round"
d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z"/>
</svg>
)
}

View File

@ -65,6 +65,7 @@ export function MobileNavigation(props: Props) {
<ul className="-my-2 divide-y divide-zinc-100 text-base text-zinc-800 dark:divide-zinc-100/5 dark:text-zinc-300">
<MobileNavItem href="/">Home</MobileNavItem>
<MobileNavItem href="/about">About</MobileNavItem>
<MobileNavItem href="/services">Services</MobileNavItem>
<MobileNavItem href="/reading">Reading</MobileNavItem>
<MobileNavItem href="/writing">Writing</MobileNavItem>
<MobileNavItem href="/projects">Projects</MobileNavItem>
@ -109,6 +110,7 @@ export function DesktopNavigation(props: Props) {
<ul className="flex rounded-full bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
<NavItem href="/">Home</NavItem>
<NavItem href="/about">About</NavItem>
<NavItem href="/services">Services</NavItem>
<NavItem href="/reading">Reading</NavItem>
<NavItem href="/writing">Writing</NavItem>
<NavItem href="/projects">Projects</NavItem>

View File

@ -1,5 +1,5 @@
export function createSlug(title: string) {
return title.toLowerCase()
.replace(/['?]+/g, '')
.replace(/['?:]+/g, '')
.replace(/[.,\s]+/g, '-')
}