Footer.jsx

  1import { forwardRef, Fragment, useState } from 'react'
  2import Link from 'next/link'
  3import { useRouter } from 'next/router'
  4import { Transition } from '@headlessui/react'
  5
  6import { Button } from '@/components/Button'
  7import { navigation } from '@/components/Navigation'
  8
  9function CheckIcon(props) {
 10  return (
 11    <svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
 12      <circle cx="10" cy="10" r="10" strokeWidth="0" />
 13      <path
 14        fill="none"
 15        strokeLinecap="round"
 16        strokeLinejoin="round"
 17        strokeWidth="1.5"
 18        d="m6.75 10.813 2.438 2.437c1.218-4.469 4.062-6.5 4.062-6.5"
 19      />
 20    </svg>
 21  )
 22}
 23
 24function FeedbackButton(props) {
 25  return (
 26    <button
 27      type="submit"
 28      className="px-3 text-sm font-medium text-zinc-600 transition hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white"
 29      {...props}
 30    />
 31  )
 32}
 33
 34const FeedbackForm = forwardRef(function FeedbackForm({ onSubmit }, ref) {
 35  return (
 36    <form
 37      ref={ref}
 38      onSubmit={onSubmit}
 39      className="absolute inset-0 flex items-center justify-center gap-6 md:justify-start"
 40    >
 41      <p className="text-sm text-zinc-600 dark:text-zinc-400">
 42        (Not hooked up) Was this page helpful?
 43      </p>
 44      <div className="group grid h-8 grid-cols-[1fr,1px,1fr] overflow-hidden rounded-full border border-zinc-900/10 dark:border-white/10">
 45        <FeedbackButton data-response="yes">Yes</FeedbackButton>
 46        <div className="bg-zinc-900/10 dark:bg-white/10" />
 47        <FeedbackButton data-response="no">No</FeedbackButton>
 48      </div>
 49    </form>
 50  )
 51})
 52
 53const FeedbackThanks = forwardRef(function FeedbackThanks(_props, ref) {
 54  return (
 55    <div
 56      ref={ref}
 57      className="absolute inset-0 flex justify-center md:justify-start"
 58    >
 59      <div className="flex items-center gap-3 rounded-full bg-emerald-50/50 py-1 pl-1.5 pr-3 text-sm text-emerald-900 ring-1 ring-inset ring-emerald-500/20 dark:bg-emerald-500/5 dark:text-emerald-200 dark:ring-emerald-500/30">
 60        <CheckIcon className="h-5 w-5 flex-none fill-emerald-500 stroke-white dark:fill-emerald-200/20 dark:stroke-emerald-200" />
 61        (Not hooked up) Thanks for your feedback!
 62      </div>
 63    </div>
 64  )
 65})
 66
 67function Feedback() {
 68  let [submitted, setSubmitted] = useState(false)
 69
 70  function onSubmit(event) {
 71    event.preventDefault()
 72
 73    // event.nativeEvent.submitter.dataset.response
 74    // => "yes" or "no"
 75
 76    setSubmitted(true)
 77  }
 78
 79  return (
 80    <div className="relative h-8">
 81      <Transition
 82        show={!submitted}
 83        as={Fragment}
 84        leaveFrom="opacity-100"
 85        leaveTo="opacity-0"
 86        leave="pointer-events-none duration-300"
 87      >
 88        <FeedbackForm onSubmit={onSubmit} />
 89      </Transition>
 90      <Transition
 91        show={submitted}
 92        as={Fragment}
 93        enterFrom="opacity-0"
 94        enterTo="opacity-100"
 95        enter="delay-150 duration-300"
 96      >
 97        <FeedbackThanks />
 98      </Transition>
 99    </div>
100  )
101}
102
103function PageLink({ label, page, previous = false }) {
104  return (
105    <>
106      <Button
107        href={page.href}
108        aria-label={`${label}: ${page.title}`}
109        variant="secondary"
110        arrow={previous ? 'left' : 'right'}
111      >
112        {label}
113      </Button>
114      <Link
115        href={page.href}
116        tabIndex={-1}
117        aria-hidden="true"
118        className="text-base font-semibold text-zinc-900 transition hover:text-zinc-600 dark:text-white dark:hover:text-zinc-300"
119      >
120        {page.title}
121      </Link>
122    </>
123  )
124}
125
126function PageNavigation() {
127  let router = useRouter()
128  let allPages = navigation.flatMap((group) => group.links)
129  let currentPageIndex = allPages.findIndex(
130    (page) => page.href === router.pathname
131  )
132
133  if (currentPageIndex === -1) {
134    return null
135  }
136
137  let previousPage = allPages[currentPageIndex - 1]
138  let nextPage = allPages[currentPageIndex + 1]
139
140  if (!previousPage && !nextPage) {
141    return null
142  }
143
144  return (
145    <div className="flex">
146      {previousPage && (
147        <div className="flex flex-col items-start gap-3">
148          <PageLink label="Previous" page={previousPage} previous />
149        </div>
150      )}
151      {nextPage && (
152        <div className="ml-auto flex flex-col items-end gap-3">
153          <PageLink label="Next" page={nextPage} />
154        </div>
155      )}
156    </div>
157  )
158}
159
160function TwitterIcon(props) {
161  return (
162    <svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
163      <path d="M16.712 6.652c.01.146.01.29.01.436 0 4.449-3.267 9.579-9.242 9.579v-.003a8.963 8.963 0 0 1-4.98-1.509 6.379 6.379 0 0 0 4.807-1.396c-1.39-.027-2.608-.966-3.035-2.337.487.097.99.077 1.467-.059-1.514-.316-2.606-1.696-2.606-3.3v-.041c.45.26.956.404 1.475.42C3.18 7.454 2.74 5.486 3.602 3.947c1.65 2.104 4.083 3.382 6.695 3.517a3.446 3.446 0 0 1 .94-3.217 3.172 3.172 0 0 1 4.596.148 6.38 6.38 0 0 0 2.063-.817 3.357 3.357 0 0 1-1.428 1.861 6.283 6.283 0 0 0 1.865-.53 6.735 6.735 0 0 1-1.62 1.744Z" />
164    </svg>
165  )
166}
167
168function GitHubIcon(props) {
169  return (
170    <svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
171      <path
172        fillRule="evenodd"
173        clipRule="evenodd"
174        d="M10 1.667c-4.605 0-8.334 3.823-8.334 8.544 0 3.78 2.385 6.974 5.698 8.106.417.075.573-.182.573-.406 0-.203-.011-.875-.011-1.592-2.093.397-2.635-.522-2.802-1.002-.094-.246-.5-1.005-.854-1.207-.291-.16-.708-.556-.01-.567.656-.01 1.124.62 1.281.876.75 1.292 1.948.93 2.427.705.073-.555.291-.93.531-1.143-1.854-.213-3.791-.95-3.791-4.218 0-.929.322-1.698.854-2.296-.083-.214-.375-1.09.083-2.265 0 0 .698-.224 2.292.876a7.576 7.576 0 0 1 2.083-.288c.709 0 1.417.096 2.084.288 1.593-1.11 2.291-.875 2.291-.875.459 1.174.167 2.05.084 2.263.53.599.854 1.357.854 2.297 0 3.278-1.948 4.005-3.802 4.219.302.266.563.78.563 1.58 0 1.143-.011 2.061-.011 2.35 0 .224.156.491.573.405a8.365 8.365 0 0 0 4.11-3.116 8.707 8.707 0 0 0 1.567-4.99c0-4.721-3.73-8.545-8.334-8.545Z"
175      />
176    </svg>
177  )
178}
179
180function DiscordIcon(props) {
181  return (
182    <svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
183      <path d="M16.238 4.515a14.842 14.842 0 0 0-3.664-1.136.055.055 0 0 0-.059.027 10.35 10.35 0 0 0-.456.938 13.702 13.702 0 0 0-4.115 0 9.479 9.479 0 0 0-.464-.938.058.058 0 0 0-.058-.027c-1.266.218-2.497.6-3.664 1.136a.052.052 0 0 0-.024.02C1.4 8.023.76 11.424 1.074 14.782a.062.062 0 0 0 .024.042 14.923 14.923 0 0 0 4.494 2.272.058.058 0 0 0 .064-.02c.346-.473.654-.972.92-1.496a.057.057 0 0 0-.032-.08 9.83 9.83 0 0 1-1.404-.669.058.058 0 0 1-.029-.046.058.058 0 0 1 .023-.05c.094-.07.189-.144.279-.218a.056.056 0 0 1 .058-.008c2.946 1.345 6.135 1.345 9.046 0a.056.056 0 0 1 .059.007c.09.074.184.149.28.22a.058.058 0 0 1 .023.049.059.059 0 0 1-.028.046 9.224 9.224 0 0 1-1.405.669.058.058 0 0 0-.033.033.056.056 0 0 0 .002.047c.27.523.58 1.022.92 1.495a.056.056 0 0 0 .062.021 14.878 14.878 0 0 0 4.502-2.272.055.055 0 0 0 .016-.018.056.056 0 0 0 .008-.023c.375-3.883-.63-7.256-2.662-10.246a.046.046 0 0 0-.023-.021Zm-9.223 8.221c-.887 0-1.618-.814-1.618-1.814s.717-1.814 1.618-1.814c.908 0 1.632.821 1.618 1.814 0 1-.717 1.814-1.618 1.814Zm5.981 0c-.887 0-1.618-.814-1.618-1.814s.717-1.814 1.618-1.814c.908 0 1.632.821 1.618 1.814 0 1-.71 1.814-1.618 1.814Z" />
184    </svg>
185  )
186}
187
188function SocialLink({ href, icon: Icon, children }) {
189  return (
190    <Link href={href} className="group">
191      <span className="sr-only">{children}</span>
192      <Icon className="h-5 w-5 fill-zinc-700 transition group-hover:fill-zinc-900 dark:group-hover:fill-zinc-500" />
193    </Link>
194  )
195}
196
197function SmallPrint() {
198  return (
199    <div className="flex flex-col items-center justify-between gap-5 border-t border-zinc-900/5 pt-8 dark:border-white/5 sm:flex-row">
200      <p className="text-xs text-zinc-600 dark:text-zinc-400">
201        &copy; Copyright {new Date().getFullYear()}. All rights reserved.
202      </p>
203      <div className="flex gap-4">
204        <SocialLink href="#" icon={TwitterIcon}>
205          Follow us on Twitter
206        </SocialLink>
207        <SocialLink href="#" icon={GitHubIcon}>
208          Follow us on GitHub
209        </SocialLink>
210        <SocialLink href="#" icon={DiscordIcon}>
211          Join our Discord server
212        </SocialLink>
213      </div>
214    </div>
215  )
216}
217
218export function Footer() {
219  let router = useRouter()
220
221  return (
222    <footer className="mx-auto max-w-2xl space-y-10 pb-16 lg:max-w-5xl">
223      <Feedback key={router.pathname} />
224      <PageNavigation />
225      <SmallPrint />
226    </footer>
227  )
228}