mirror of
https://github.com/r-freeman/portfolio.git
synced 2024-11-21 20:45:41 +00:00
Added new page
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 2m39s
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 2m39s
This commit is contained in:
parent
5e8da79a82
commit
6c0100e9a3
@ -4,7 +4,7 @@ import React from 'react';
|
|||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Reading - Ryan Freeman',
|
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 = {
|
type Book = {
|
||||||
@ -69,7 +69,7 @@ export default async function Reading() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleLayout
|
<SimpleLayout
|
||||||
heading="What's on my bookshelf"
|
heading="Books I'm reading at the moment"
|
||||||
description={metadata.description}
|
description={metadata.description}
|
||||||
gradient="bg-gradient-to-r from-sky-400 to-blue-500">
|
gradient="bg-gradient-to-r from-sky-400 to-blue-500">
|
||||||
<ul
|
<ul
|
||||||
|
95
app/services/page.tsx
Normal file
95
app/services/page.tsx
Normal 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 you’re 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>
|
||||||
|
)
|
||||||
|
}
|
@ -5,6 +5,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
const urls = [
|
const urls = [
|
||||||
'https://ryanfreeman.dev/',
|
'https://ryanfreeman.dev/',
|
||||||
'https://ryanfreeman.dev/about',
|
'https://ryanfreeman.dev/about',
|
||||||
|
'https://ryanfreeman.dev/services',
|
||||||
'https://ryanfreeman.dev/reading',
|
'https://ryanfreeman.dev/reading',
|
||||||
'https://ryanfreeman.dev/writing',
|
'https://ryanfreeman.dev/writing',
|
||||||
'https://ryanfreeman.dev/projects',
|
'https://ryanfreeman.dev/projects',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {OuterContainer, InnerContainer} from './Container'
|
import {InnerContainer, OuterContainer} from './Container'
|
||||||
import {NavLink} from '@/components/ui/Navigation'
|
import {NavLink} from '@/components/ui/Navigation'
|
||||||
import {SpotifyPlayer} from '@/components/ui/SpotifyPlayer'
|
import {SpotifyPlayer} from '@/components/ui/SpotifyPlayer'
|
||||||
import {SocialLink} from '@/components/ui/SocialLink'
|
import {SocialLink} from '@/components/ui/SocialLink'
|
||||||
@ -12,12 +12,15 @@ export function Footer() {
|
|||||||
<OuterContainer>
|
<OuterContainer>
|
||||||
<div className="border-t border-zinc-100 pt-10 pb-16 dark:border-zinc-700/40">
|
<div className="border-t border-zinc-100 pt-10 pb-16 dark:border-zinc-700/40">
|
||||||
<InnerContainer>
|
<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-col items-center justify-between gap-6 mt-12">
|
||||||
<div
|
<div
|
||||||
className="flex flex-wrap justify-center gap-6 text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
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="/">Home</NavLink>
|
||||||
<NavLink href="/about">About</NavLink>
|
<NavLink href="/about">About</NavLink>
|
||||||
|
<NavLink href="/services">Services</NavLink>
|
||||||
<NavLink href="/reading">Reading</NavLink>
|
<NavLink href="/reading">Reading</NavLink>
|
||||||
<NavLink href="/writing">Writing</NavLink>
|
<NavLink href="/writing">Writing</NavLink>
|
||||||
<NavLink href="/projects">Projects</NavLink>
|
<NavLink href="/projects">Projects</NavLink>
|
||||||
|
11
components/icons/AppIcon.tsx
Normal file
11
components/icons/AppIcon.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
12
components/icons/CloudIcon.tsx
Normal file
12
components/icons/CloudIcon.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
11
components/icons/CodeIcon.tsx
Normal file
11
components/icons/CodeIcon.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
11
components/icons/DatabaseIcon.tsx
Normal file
11
components/icons/DatabaseIcon.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
11
components/icons/EmailIcon.tsx
Normal file
11
components/icons/EmailIcon.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
11
components/icons/RocketIcon.tsx
Normal file
11
components/icons/RocketIcon.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
11
components/icons/ShieldIcon.tsx
Normal file
11
components/icons/ShieldIcon.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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">
|
<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="/">Home</MobileNavItem>
|
||||||
<MobileNavItem href="/about">About</MobileNavItem>
|
<MobileNavItem href="/about">About</MobileNavItem>
|
||||||
|
<MobileNavItem href="/services">Services</MobileNavItem>
|
||||||
<MobileNavItem href="/reading">Reading</MobileNavItem>
|
<MobileNavItem href="/reading">Reading</MobileNavItem>
|
||||||
<MobileNavItem href="/writing">Writing</MobileNavItem>
|
<MobileNavItem href="/writing">Writing</MobileNavItem>
|
||||||
<MobileNavItem href="/projects">Projects</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">
|
<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="/">Home</NavItem>
|
||||||
<NavItem href="/about">About</NavItem>
|
<NavItem href="/about">About</NavItem>
|
||||||
|
<NavItem href="/services">Services</NavItem>
|
||||||
<NavItem href="/reading">Reading</NavItem>
|
<NavItem href="/reading">Reading</NavItem>
|
||||||
<NavItem href="/writing">Writing</NavItem>
|
<NavItem href="/writing">Writing</NavItem>
|
||||||
<NavItem href="/projects">Projects</NavItem>
|
<NavItem href="/projects">Projects</NavItem>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export function createSlug(title: string) {
|
export function createSlug(title: string) {
|
||||||
return title.toLowerCase()
|
return title.toLowerCase()
|
||||||
.replace(/['?]+/g, '')
|
.replace(/['?:]+/g, '')
|
||||||
.replace(/[.,\s]+/g, '-')
|
.replace(/[.,\s]+/g, '-')
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user