Skip to content

Commit

Permalink
Merge pull request #17 from EmilEinarsen/feat/blog
Browse files Browse the repository at this point in the history
feat: ✨ blog like news pages
  • Loading branch information
EmilEinarsen authored May 1, 2023
2 parents 91942fd + a6548d2 commit e8c33c1
Show file tree
Hide file tree
Showing 58 changed files with 995 additions and 426 deletions.
54 changes: 54 additions & 0 deletions app/components/app/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { HomeIcon } from '@heroicons/react/24/solid'
import { Link } from '@remix-run/react'
import React from 'react'
import { useRouteData } from '~/hooks/useRouteData'
import { clsx } from '~/utils/utils'

interface BreadcrumbProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {}

export const Breadcrumb = (props: BreadcrumbProps) => {
const { post } = useRouteData()

return (
<nav
{...props}
className={clsx(
'w-min',
props.className
)}
aria-label="Breadcrumb"
>
<ol role="list" className="flex items-center space-x-4">
<li>
<div>
<Link to='/' className="text-gray-400 hover:text-gray-500 whitespace-nowrap">
<HomeIcon className="flex-shrink-0 w-5 h-5" aria-hidden="true" />
<span className="sr-only">Home</span>
</Link>
</div>
</li>
{post?.breadcrumbs.map((page, i, pages) => (
<li key={page.name}>
<div className="flex items-center">
<svg
className="flex-shrink-0 w-5 h-5 text-gray-300"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
</svg>
<Link
to={page.href}
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700 whitespace-nowrap aria-[current='page']:"
aria-current={i === pages.length-1 ? 'page' : undefined}
>
{page.name}
</Link>
</div>
</li>
))}
</ol>
</nav>
)
}
3 changes: 1 addition & 2 deletions app/components/app/Logo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Link, LinkProps } from '@remix-run/react';
import { useRouteData } from '~/hooks/useRouteData'
import { clsx } from '~/utils/utils';
import { useRouteData } from '~/hooks/useRouteData';
import { Image } from '../core/image';

interface LogoProps extends Omit<LinkProps, 'to'> {
Expand Down
72 changes: 72 additions & 0 deletions app/components/app/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ArrowLongLeftIcon, ArrowLongRightIcon } from '@heroicons/react/24/solid'
import { Link, useLocation } from '@remix-run/react'
import { useMemo } from 'react'
import { clsx } from '~/utils/utils'

interface PaginationProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {
currentPage: number
maxPage: number
}

export const Pagination = ({ currentPage, maxPage, ...props }: PaginationProps) => {
const { pathname } = useLocation()

const pagination = useMemo(() => {
const previousPage = Math.max(currentPage - 1, 1);
const nextPage = Math.min(currentPage + 1, maxPage);

return {
previous: {
disabled: previousPage <= 1,
link: `${pathname}?page=${previousPage}`
},
pages: [...Array(maxPage+1).keys()].slice(1).map(i => ({ label: i, value: i, href: `${pathname}?page=${i}` })),
next: {
disabled: currentPage === maxPage, // or some empty state indicator
link: `${pathname}?page=${nextPage}`
}
}
}, [currentPage, maxPage]);

return (
<nav className={clsx(
'flex items-center justify-between px-4 border-t border-gray-200 sm:px-0',
props.className
)}>
<div className="flex flex-1 w-0 -mt-px">
<Link
to={pagination.previous.link}
className="inline-flex items-center pt-4 pr-1 text-sm font-medium text-gray-500 border-transparent hover:border-gray-300 hover:text-gray-700"
aria-disabled={pagination.previous.disabled}
>
<ArrowLongLeftIcon className="w-5 h-5 mr-3 text-gray-400" aria-hidden="true" />
Previous
</Link>
</div>
<div className="hidden md:-mt-px md:flex">
{pagination.pages.map(page =>
<Link
to={page.href}
className={clsx(
"inline-flex items-center border-t-2 px-4 pt-4 text-sm font-medium",
currentPage === page.value && 'border-indigo-500 text-indigo-600',
currentPage !== page.value && 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
)}
>
{page.label}
</Link>
)}
</div>
<div className="flex justify-end flex-1 w-0 -mt-px">
<Link
to={pagination.next.link}
className="inline-flex items-center pt-4 pr-1 text-sm font-medium text-gray-500 border-t-2 border-transparent hover:border-gray-300 hover:text-gray-700"
aria-disabled={pagination.next.disabled}
>
Next
<ArrowLongRightIcon className="w-5 h-5 ml-3 text-gray-400" aria-hidden="true" />
</Link>
</div>
</nav>
)
}
68 changes: 68 additions & 0 deletions app/components/app/Post.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Link } from '@remix-run/react'
import { clsx, dateFormat } from '~/utils/utils'
import { Image } from '../core/image'

import type { Post } from '~/loaders/groq-fragments/documents/blog-post'

interface BlogPostProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {
post: Post
withoutImage?: boolean
}

export const BlogPost = ({ post, withoutImage, ...props }: BlogPostProps) => {
return (
<article
key={post.id}
{...props}
className={clsx(
'relative flex flex-col gap-8 isolate lg:flex-row',
props.className
)}
>
<div className="relative aspect-[16/9] sm:aspect-[2/1] lg:aspect-square lg:w-48 lg:shrink-0">
<Image
image={post.image!}
alt=""
background
className="object-cover rounded-2xl bg-gray-50"
/>
<div className="absolute inset-0 rounded-2xl ring-1 ring-inset ring-gray-900/10" />
</div>
<div>
<div className="flex items-center text-xs gap-x-4">
<time dateTime={post.publishedAt} className="text-gray-500">
{dateFormat(post.publishedAt)}
</time>
<p
/* href={post.category.href} */
className="relative z-10 rounded-full bg-gray-50 px-3 py-1.5 font-medium text-gray-600"/* hover:bg-gray-100 */
>
{post.category.title}
</p>
</div>
<div className="relative max-w-xl group">
<h3 className="mt-3 text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600">
<Link to={post.href.slug}>
<span className="absolute inset-0" />
{post.title}
</Link>
</h3>
<p className="mt-5 text-sm leading-6 text-gray-600">{post.description}</p>
</div>
<div className="flex pt-6 mt-6 border-t border-gray-900/5">
<div className="relative flex items-center gap-x-4">
<Image image={post.author.image} className="w-10 h-10 rounded-full bg-gray-50" />
<div className="text-sm leading-6">
<p className="font-semibold text-gray-900">
<span /* href={post.author.slug} */>
{post.author.name}
</span>
</p>
<p className="text-gray-600">{post.author.role}</p>
</div>
</div>
</div>
</div>
</article>
)
}
2 changes: 1 addition & 1 deletion app/components/core/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const Image = (
loading='lazy'
{...props}
className={clsx(
background ? 'absolute top-0 left-0 w-full h-full select-none z-0' : 'block max-w-full w-full h-auto',
background ? 'absolute top-0 left-0 w-full h-full select-none z-0' : 'block max-w-full',
background && image.type !== 'image/svg+xml' && 'object-cover',
props.className,
)}
Expand Down
26 changes: 26 additions & 0 deletions app/components/modules/blog-posts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ModuleProps } from '.'
import { BlogPost } from '../app/Post'

const BlogPosts = (props: ModuleProps<'blog-posts'>) => {
return (
<section>
<div className='max-w-6xl py-16 mx-auto sm:py-24 sm:px-6'>
<div className='mx-auto mb-16 prose prose-xl text-center sm:mb-24 prose-gray'>
<h2>{props.data.title}</h2>
{props.data.subtitle && <p>{props.data.subtitle}</p>}
</div>
<div className='grid gap-10 sm:grid-cols-2'>
{props.data.posts.map(post =>
<BlogPost
key={post.id}
post={post}
className='col-span-1'
/>
)}
</div>
</div>
</section>
)
}

export default BlogPosts
44 changes: 29 additions & 15 deletions app/components/modules/contact-form.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { EnvelopeIcon, MapPinIcon, PhoneIcon } from '@heroicons/react/24/solid'
import { Form, Link, useActionData, useNavigation, useSubmit } from '@remix-run/react'
import { Form, Link, useNavigation, useSearchParams, useSubmit } from '@remix-run/react'
import React, { useEffect, useRef } from 'react'
import ReCAPTCHA from "react-google-recaptcha"
import { LOCALE, Locale } from 'sanity/lib/i18n'

import { useRouteData } from '~/hooks/useRouteData'
import { Company } from '~/loaders/groq-fragments/documents/site'
import { getENV } from '~/utils/getENV'
import { ActionData } from '~/routes/_app.($lang).$'
import { getENV } from '~/loaders/groq-fragments/utils/getENV'
import { clsx } from '~/utils/utils'
import { ModuleProps } from '.'
import { Alert } from '../core/alert'

const T = {
[LOCALE.se]: {
title: 'Kontakta oss',
subtitle: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
},
[LOCALE.en]: {
title: 'Contact us',
subtitle: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'

},
} as const satisfies { [K in Locale]: Record<string, string> }

const FORM_FEEDBACK = {
success: {
state: 'success',
Expand Down Expand Up @@ -49,23 +61,27 @@ const getContactInfo = (company: Company) => [
},
].filter((v): v is Exclude<typeof v, undefined> => !!v)

const ContactForm = ({ data }: ModuleProps<'contact-form'>) => {
const { site } = useRouteData()
const actionData = useActionData() as ActionData;
const ContactForm = (props: Partial<ModuleProps<'contact-form'>>) => {
const { site, lang } = useRouteData()
const { title = T[lang].title, subtitle = T[lang].subtitle } = props.data ?? {}

const transition = useNavigation();
const [ search ] = useSearchParams()
const status = +(search.get('status')??NaN)

const state = transition.state === 'submitting'
? "submitting"
: actionData?.status === 'success'
: status === 200
? "success"
: actionData?.status === 'error'
: status === 400
? "error"
: "idle";

const formRef = useRef<HTMLFormElement>(null);
const successRef = useRef<HTMLHeadingElement>(null);

useEffect(() => {
if (state === 'success' || state === 'error') {
if (state === 'success') {
formRef.current?.reset()
successRef.current?.focus();
}
Expand All @@ -84,12 +100,12 @@ const ContactForm = ({ data }: ModuleProps<'contact-form'>) => {

formData.set("g-recaptcha-response", captchaToken);

submit(formData, { method: 'POST' });
submit(formData, { action: '/api/contact', method: 'POST', replace: true, preventScrollReset: true });
}

return (
<section>
<div className='max-w-6xl px-4 py-24 mx-auto max-sm:py-10 sm:px-6' id="contact-info">
<div className='max-w-6xl px-4 py-24 mx-auto sm:px-6' id="contact-info">
<div className='flex flex-wrap justify-around gap-10'>
{getContactInfo(site.company).map(info =>
<div key={info.title} className='text-center w-72'>
Expand All @@ -105,13 +121,11 @@ const ContactForm = ({ data }: ModuleProps<'contact-form'>) => {
<div className="relative bg-blue-50">
<span id="contact-form" className='absolute -top-20' />
<div className='relative max-w-screen-sm px-4 py-24 mx-auto sm:py-16 sm:px-6'>
<h2 className='mb-4 text-4xl font-extrabold tracking-tight text-center text-gray-900'>{data.title}</h2>
<p className='mb-8 font-light text-center lg:mb-16 sm:text-xl'>{data.subtitle}</p>
<h2 className='mb-4 text-4xl font-extrabold tracking-tight text-center text-gray-900'>{title}</h2>
<p className='mb-8 font-light text-center lg:mb-16 sm:text-xl'>{subtitle}</p>
<div>
<Form
ref={formRef}
replace
method="post"
aria-hidden={state === "success"}
className={clsx(
'space-y-8 transition-all',
Expand Down
1 change: 0 additions & 1 deletion app/components/modules/cta.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Link } from '@remix-run/react'
import React from 'react'

import { Image } from '~/components/core/image'

Expand Down
6 changes: 4 additions & 2 deletions app/components/modules/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import CTA from './cta'
import Partners from './partners'
import TextImage from './text-image'
import ContactForm from './contact-form'
import BlogPosts from './blog-posts'

import type { Modules } from '~/loaders/groq-fragments/objects/modules'
import type { Modules } from '~/loaders/groq-fragments/modules/modules'

export interface ModuleProps<T extends Modules['_type'] = Modules['_type']> {
index: number;
Expand All @@ -20,7 +21,8 @@ const modules = {
cta: CTA,
partners: Partners,
'text-image': TextImage,
'contact-form': ContactForm
'contact-form': ContactForm,
'blog-posts': BlogPosts
} as { [k in Modules['_type']]: React.FunctionComponent<ModuleProps> };

export const Module = ({
Expand Down
Loading

1 comment on commit e8c33c1

@vercel
Copy link

@vercel vercel bot commented on e8c33c1 May 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

hagatun – ./

hagatun.vercel.app
hagatun-hagatun.vercel.app
hagatun-git-main-hagatun.vercel.app
hagatun.se

Please sign in to comment.