Repository: flexbox/weshipit.today
Pull request #227: feat: 🎸 add react native glossary
Author: login: MatthysDev
Created: 2025-04-18T14:10:49Z | Merged: 2025-04-24T09:56:56Z

### Checklist before merge

- [ ] The first commit is nice to make ChatGPT happy
- [ ] Merge the pull request using **Squash and merge**



Diff:
diff --git a/apps/weshipit/components/GlossaryCTA.tsx b/apps/weshipit/components/GlossaryCTA.tsx
new file mode 100644
index 0000000..b641a7a
--- /dev/null
+++ b/apps/weshipit/components/GlossaryCTA.tsx
@@ -0,0 +1,39 @@
+import { Card, LinkButton, Prose, Text } from '@weshipit/ui';
+import { linksApi } from '../pages/api/links';
+
+export function GlossaryCTA() {
+  return (
+    <Card className="flex flex-col items-start" size="lg">
+      <Prose>
+        <h2>Unlock your app's full potential</h2>
+        <p>
+          Don't let your app become another statistic. 99% of apps fail due to
+          poor implementation and design.
+        </p>
+        <p>Our React Native experts can help you:</p>
+        <ul>
+          <li>Optimize performance for buttery-smooth UX</li>
+          <li>Create intuitive interfaces users love</li>
+          <li>Accelerate development with proven patterns</li>
+        </ul>
+
+        <LinkButton
+          href={linksApi?.cal?.ONBOARDING || '/contact'}
+          size="xl"
+          variant="primary"
+          className="mt-4 w-full justify-center not-prose"
+        >
+          Book your consultation
+        </LinkButton>
+
+        <Text
+          as="p"
+          variant="c1"
+          className="mt-4 text-slate-400 dark:text-slate-300 text-center"
+        >
+          No commitment required. Discover how we can transform your app.
+        </Text>
+      </Prose>
+    </Card>
+  );
+}
diff --git a/apps/weshipit/pages/api/glossary.ts b/apps/weshipit/pages/api/glossary.ts
new file mode 100644
index 0000000..72b0224
--- /dev/null
+++ b/apps/weshipit/pages/api/glossary.ts
@@ -0,0 +1,61 @@
+import { client as prismicClient } from './prismic';
+
+export interface GlossaryTerm {
+  id: string;
+  uid: string | null;
+  data: {
+    title: string;
+    description: any;
+    related_to: any;
+    related_articles?: any[];
+    [key: string]: any;
+  };
+}
+
+export async function getAllGlossaryTerms() {
+  try {
+    const glossaryTerms = await prismicClient.getAllByType('glossary');
+
+    return {
+      glossaryTerms: glossaryTerms as unknown as GlossaryTerm[],
+    };
+  } catch (error) {
+    console.error('getAllGlossaryTerms -> error', error);
+    return { glossaryTerms: [] };
+  }
+}
+
+export async function getGlossaryTermByUID(uid: string) {
+  try {
+    const term = await prismicClient.getByUID('glossary', uid);
+    return term as unknown as GlossaryTerm;
+  } catch (error) {
+    console.error('getGlossaryTermByUID -> error', error);
+    return null;
+  }
+}
+
+export async function getPrevNextGlossaryTerms(currentUID: string) {
+  try {
+    const allTerms = await prismicClient.getAllByType('glossary');
+
+    // Sort terms alphabetically by title
+    allTerms.sort((a, b) => a.data.title.localeCompare(b.data.title));
+
+    const currentIndex = allTerms.findIndex((term) => term.uid === currentUID);
+
+    return {
+      previousTerm:
+        currentIndex > 0
+          ? (allTerms[currentIndex - 1] as unknown as GlossaryTerm)
+          : null,
+      nextTerm:
+        currentIndex < allTerms.length - 1
+          ? (allTerms[currentIndex + 1] as unknown as GlossaryTerm)
+          : null,
+    };
+  } catch (error) {
+    console.error('getPrevNextGlossaryTerms -> error', error);
+    return { previousTerm: null, nextTerm: null };
+  }
+}
diff --git a/apps/weshipit/pages/react-native-glossary/[uid].tsx b/apps/weshipit/pages/react-native-glossary/[uid].tsx
new file mode 100644
index 0000000..39e48fa
--- /dev/null
+++ b/apps/weshipit/pages/react-native-glossary/[uid].tsx
@@ -0,0 +1,255 @@
+import { Layout } from '../../components/layout';
+import { PrismicRichText } from '@prismicio/react';
+import { asText } from '@prismicio/client';
+import Link from 'next/link';
+import { LinkButton } from '@weshipit/ui';
+import { GetStaticPaths, GetStaticProps } from 'next';
+import { GlossaryTerm, getAllGlossaryTerms } from '../api/glossary';
+import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
+import { client as prismicClient } from '../api/prismic';
+import { GlossaryCTA } from '../../components/GlossaryCTA';
+import { slugify } from '../../utils/slugify';
+
+interface GlossaryTermPageProps {
+  term: GlossaryTerm;
+  previousTerm: GlossaryTerm | null;
+  nextTerm: GlossaryTerm | null;
+  relatedArticles: any[];
+  termsByTitle: Record<string, GlossaryTerm>;
+}
+
+export const getStaticPaths: GetStaticPaths = async () => {
+  const { glossaryTerms } = await getAllGlossaryTerms();
+
+  const paths = glossaryTerms
+    .filter((term) => term.data.title)
+    .map((term) => ({
+      params: { uid: slugify(term.data.title) },
+    }));
+
+  return {
+    paths,
+    fallback: 'blocking',
+  };
+};
+
+export const getStaticProps: GetStaticProps = async ({ params }) => {
+  const slug = params?.uid as string;
+  if (!slug) {
+    return { notFound: true };
+  }
+
+  // Get all glossary terms
+  const { glossaryTerms } = await getAllGlossaryTerms();
+
+  // Find the term that matches the slug
+  const term = glossaryTerms.find((term) => slugify(term.data.title) === slug);
+
+  if (!term) {
+    return { notFound: true };
+  }
+
+  // Sort terms alphabetically for prev/next navigation
+  const sortedTerms = [...glossaryTerms].sort((a, b) =>
+    a.data.title.localeCompare(b.data.title),
+  );
+
+  const currentIndex = sortedTerms.findIndex(
+    (t) => slugify(t.data.title) === slug,
+  );
+
+  const previousTerm = currentIndex > 0 ? sortedTerms[currentIndex - 1] : null;
+  const nextTerm =
+    currentIndex < sortedTerms.length - 1
+      ? sortedTerms[currentIndex + 1]
+      : null;
+
+  // Create a map of terms by title for quick lookup
+  const termsByTitle: Record<string, GlossaryTerm> = {};
+  glossaryTerms.forEach((term) => {
+    if (term.data.title) {
+      termsByTitle[term.data.title] = term as unknown as GlossaryTerm;
+    }
+  });
+
+  // Fetch related articles from Prismic
+  let relatedArticles: any[] = [];
+  if (term.data.related_articles && term.data.related_articles.length > 0) {
+    try {
+      const articlePromises = term.data.related_articles.map(
+        async (article: any) => {
+          if (article.article && article.article.id) {
+            return await prismicClient.getByID(article.article.id);
+          }
+          return null;
+        },
+      );
+
+      const articles = await Promise.all(articlePromises);
+      relatedArticles = articles.filter(Boolean);
+    } catch (error) {
+      console.error('Error fetching related articles', error);
+    }
+  }
+
+  return {
+    props: {
+      term,
+      previousTerm,
+      nextTerm,
+      relatedArticles,
+      termsByTitle,
+    },
+    revalidate: 60,
+  };
+};
+
+export default function GlossaryTermPage({
+  term,
+  previousTerm,
+  nextTerm,
+  relatedArticles,
+  termsByTitle,
+}: GlossaryTermPageProps) {
+  if (!term) {
+    return null;
+  }
+
+  return (
+    <Layout
+      seoTitle={`${term.data.title} | React Native Glossary | weshipit.today`}
+      seoDescription={`Learn about ${term.data.title} in React Native development.`}
+      ogImageTitle={`${term.data.title} | React Native Glossary`}
+      withHeader
+      withFooter
+      withContainer
+    >
+      <article className="py-12">
+        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
+          <div className="mb-8">
+            <Link
+              href="/react-native-glossary"
+              className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 flex items-center"
+            >
+              <ChevronLeftIcon className="h-4 w-4 mr-1" />
+              Back to Glossary
+            </Link>
+          </div>
+
+          <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
+            <section className="lg:col-span-2">
+              <h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-6">
+                {term.data.title}
+              </h1>
+
+              <div className="prose dark:prose-invert max-w-none">
+                <PrismicRichText field={term.data.description} />
+              </div>
+
+              {term.data.related_to && term.data.related_to.length > 0 && (
+                <div className="mt-8">
+                  <h2 className="text-xl font-medium text-gray-900 dark:text-white mb-4">
+                    Related Terms
+                  </h2>
+                  <div className="flex flex-wrap gap-2">
+                    {term.data.related_to.map((related: any, index: number) => {
+                      // Get the text of the related term
+                      const relatedText = asText(related);
+                      // Look up the term in our map of terms by title
+                      const matchingTerm = relatedText
+                        ? termsByTitle[relatedText]
+                        : undefined;
+
+                      return (
+                        <span
+                          key={index}
+                          className="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-0.5 text-sm font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-200"
+                        >
+                          {matchingTerm ? (
+                            <Link
+                              href={`/react-native-glossary/${slugify(matchingTerm.data.title)}`}
+                              className="hover:text-blue-600 dark:hover:text-blue-400"
+                            >
+                              {relatedText}
+                            </Link>
+                          ) : (
+                            relatedText
+                          )}
+                        </span>
+                      );
+                    })}
+                  </div>
+                </div>
+              )}
+
+              {relatedArticles && relatedArticles.length > 0 && (
+                <div className="mt-12">
+                  <h2 className="text-xl font-medium text-gray-900 dark:text-white mb-4">
+                    Relevant Articles
+                  </h2>
+                  <div className="space-y-4">
+                    {relatedArticles.map((article) => (
+                      <div
+                        key={article.id}
+                        className="border border-gray-200 dark:border-gray-700 rounded-lg p-4"
+                      >
+                        <h3 className="text-lg font-medium text-gray-900 dark:text-white">
+                          <Link
+                            href={`/blog/${article.uid}`}
+                            className="hover:text-blue-600 dark:hover:text-blue-400"
+                          >
+                            {article.data.title}
+                          </Link>
+                        </h3>
+                        {article.data.excerpt && (
+                          <p className="mt-2 text-gray-500 dark:text-gray-400">
+                            {article.data.excerpt}
+                          </p>
+                        )}
+                      </div>
+                    ))}
+                  </div>
+                </div>
+              )}
+
+              {/* Previous / Next Navigation */}
+              <div className="mt-20 border-t border-gray-200 dark:border-gray-700 pt-8 flex justify-between">
+                <div>
+                  {previousTerm && (
+                    <LinkButton
+                      href={`/react-native-glossary/${slugify(previousTerm.data.title)}`}
+                      size="lg"
+                      variant="ghost"
+                      className="flex items-center"
+                    >
+                      <ChevronLeftIcon className="h-4 w-4 mr-2" />
+                      {previousTerm.data.title}
+                    </LinkButton>
+                  )}
+                </div>
+                <div>
+                  {nextTerm && (
+                    <LinkButton
+                      href={`/react-native-glossary/${slugify(nextTerm.data.title)}`}
+                      size="lg"
+                      variant="ghost"
+                      className="flex items-center"
+                    >
+                      {nextTerm.data.title}
+                      <ChevronRightIcon className="h-4 w-4 ml-2" />
+                    </LinkButton>
+                  )}
+                </div>
+              </div>
+            </section>
+
+            {/* Call to Action Card */}
+            <aside className="sticky top-8 h-fit lg:col-span-1">
+              <GlossaryCTA />
+            </aside>
+          </div>
+        </div>
+      </article>
+    </Layout>
+  );
+}
diff --git a/apps/weshipit/pages/react-native-glossary/index.tsx b/apps/weshipit/pages/react-native-glossary/index.tsx
new file mode 100644
index 0000000..52c79b6
--- /dev/null
+++ b/apps/weshipit/pages/react-native-glossary/index.tsx
@@ -0,0 +1,155 @@
+import { Layout } from '../../components/layout';
+import { GlossaryTerm, getAllGlossaryTerms } from '../api/glossary';
+import { PrismicRichText } from '@prismicio/react';
+import { asText } from '@prismicio/client';
+import Link from 'next/link';
+import { useEffect, useState } from 'react';
+import { GlossaryCTA } from '../../components/GlossaryCTA';
+import { slugify } from '../../utils/slugify';
+
+interface GlossaryPageProps {
+  glossaryTerms: GlossaryTerm[];
+}
+
+export async function getStaticProps() {
+  const { glossaryTerms } = await getAllGlossaryTerms();
+
+  // Group terms by their first letter
+  const groupedTerms: { [key: string]: GlossaryTerm[] } = {};
+  glossaryTerms.forEach((term) => {
+    if (!term.data.title) return;
+
+    const firstLetter = term.data.title.charAt(0).toUpperCase();
+    const category = /^[0-9]/.test(firstLetter) ? '#' : firstLetter;
+
+    if (!groupedTerms[category]) {
+      groupedTerms[category] = [];
+    }
+
+    groupedTerms[category].push(term as unknown as GlossaryTerm);
+  });
+
+  return {
+    props: {
+      groupedTerms,
+    },
+  };
+}
+
+export default function ReactNativeGlossary({
+  groupedTerms,
+}: GlossaryPageProps & { groupedTerms: { [key: string]: GlossaryTerm[] } }) {
+  const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
+  const [activeLetters, setActiveLetters] = useState<string[]>([]);
+
+  useEffect(() => {
+    // Set active letters based on available terms
+    setActiveLetters(Object.keys(groupedTerms));
+  }, [groupedTerms]);
+
+  return (
+    <Layout
+      seoTitle="React Native Glossary | weshipit.today"
+      seoDescription="A comprehensive glossary of React Native terms and concepts to help you better understand React Native development."
+      ogImageTitle="React Native Glossary"
+      withHeader
+      withFooter
+      withContainer
+    >
+      <article className="py-12">
+        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
+          <header className="lg:text-center">
+            <h1 className="mt-2 text-3xl font-bold leading-8 tracking-tight text-gray-900 dark:text-white sm:text-4xl">
+              React Native Glossary
+            </h1>
+            <p className="mt-4 max-w-2xl text-xl text-gray-500 dark:text-gray-400 lg:mx-auto">
+              A glossary of all the terms around React Native development.
+            </p>
+          </header>
+
+          {/* Alphabet Navigation */}
+          <nav className="mt-8 flex flex-wrap justify-center gap-2 sm:gap-4">
+            {alphabet.map((letter) => {
+              const isActive = activeLetters.includes(letter);
+              return (
+                <Link
+                  key={letter}
+                  href={isActive ? `#${letter}` : '#'}
+                  className={`inline-flex h-10 w-10 items-center justify-center rounded-md text-lg font-medium shadow-sm
+                    ${
+                      isActive
+                        ? 'bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 dark:bg-blue-700 dark:text-white dark:hover:bg-blue-800 dark:focus-visible:outline-blue-700'
+                        : 'border border-gray-300 bg-white text-gray-500 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700'
+                    }`}
+                  aria-disabled={!isActive}
+                >
+                  {letter}
+                </Link>
+              );
+            })}
+          </nav>
+
+          <div className="mt-12 grid grid-cols-1 gap-8 lg:grid-cols-3">
+            <section className="lg:col-span-2">
+              {activeLetters.sort().map((letter) => (
+                <div key={letter} id={letter} className="mb-12">
+                  <h2 className="mb-6 text-4xl font-bold text-gray-900 dark:text-white">
+                    {letter}
+                  </h2>
+
+                  <dl className="space-y-8">
+                    {groupedTerms[letter]?.map((item) => (
+                      <div
+                        key={item.id}
+                        className="relative border-b border-gray-200 dark:border-gray-700 pb-6"
+                      >
+                        <dt>
+                          <h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-white">
+                            <Link
+                              href={`/react-native-glossary/${slugify(item.data.title)}`}
+                              className="hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer"
+                            >
+                              {item.data.title}
+                            </Link>
+                          </h3>
+                        </dt>
+                        <dd className="mt-2 text-base text-gray-500 dark:text-gray-400">
+                          <PrismicRichText field={item.data.description} />
+                        </dd>
+                        {item.data.related_to &&
+                          item.data.related_to.length > 0 && (
+                            <div className="mt-4">
+                              <span className="text-sm font-medium text-gray-500 dark:text-gray-400">
+                                Related to:
+                              </span>
+                              <div className="mt-1 flex flex-wrap gap-2">
+                                {item.data.related_to.map(
+                                  (related: any, index: number) => (
+                                    <span
+                                      key={index}
+                                      className="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-0.5 text-sm font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-200"
+                                    >
+                                      {asText(related)}
+                                    </span>
+                                  ),
+                                )}
+                              </div>
+                            </div>
+                          )}
+                      </div>
+                    ))}
+                  </dl>
+                </div>
+              ))}
+            </section>
+
+            {/* Call to Action Card */}
+            <aside className="sticky top-8 h-fit lg:col-span-1">
+              <GlossaryCTA />
+            </aside>
+          </div>
+        </div>
+      </article>
+    </Layout>
+  );
+}
diff --git a/apps/weshipit/utils/slugify.ts b/apps/weshipit/utils/slugify.ts
new file mode 100644
index 0000000..4477ff3
--- /dev/null
+++ b/apps/weshipit/utils/slugify.ts
@@ -0,0 +1,10 @@
+export function slugify(text: string): string {
+  return text
+    .toString()
+    .toLowerCase()
+    .trim()
+    .replace(/\s+/g, '-')
+    .replace(/&/g, '-and-')
+    .replace(/[^\w\-]+/g, '')
+    .replace(/\-\-+/g, '-');
+}
diff --git a/libs/ui/src/lib/footer/footer.tsx b/libs/ui/src/lib/footer/footer.tsx
index e4a1107..a83b11c 100644
--- a/libs/ui/src/lib/footer/footer.tsx
+++ b/libs/ui/src/lib/footer/footer.tsx
@@ -82,6 +82,7 @@ const navigation = {
     { href: '/react-native-starters', name: 'React Native Starters' },
     { href: '/audit', name: 'React Native Audit' },
     { href: '/french-react-native-apps', name: 'React Native Apps 🇫🇷 ' },
+    { href: '/react-native-glossary', name: 'React Native Glossary' },
   ],
 };
 


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a new React Native Glossary feature. The changes include:
- Adding a new glossary link to the footer navigation.
- Creating a new glossary page that displays glossary terms.
- Adding an API endpoint to fetch glossary terms from Prismic.

### Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

| File | Description |
| ---- | ----------- |
| libs/ui/src/lib/footer/footer.tsx | Adds "React Native Glossary" link to footer navigation. |
| apps/weshipit/pages/glossary.tsx | Implements the glossary page with term listing and rich text rendering. |
| apps/weshipit/pages/api/glossary.ts | Introduces API endpoint to retrieve glossary terms from Prismic. |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:03:26Z
user: login: flexbox
body: @MatthysDev review done
avoid duplication 🙏
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-23T12:31:56Z

Review comments:
diff_hunk: @@ -0,0 +1,23 @@
+import { client as prismicClient } from './prismic';
+
+export interface GlossaryTerm {
+  id: string;
+  data: {
+    title: string;
+    description: any;
+    related_to: any;
+  };
+}
+
+export async function getAllGlossaryTerms() {
+  try {
+    const glossaryTerms = await prismicClient.getAllByType('glossary');
+
+    return {
+      glossaryTerms: glossaryTerms ? glossaryTerms : [],
+    };
+  } catch (error) {
+    console.error('getAllGlossaryTerms -> error', error);
+    return error;
path: apps/weshipit/pages/api/glossary.ts
user: login: Copilot
body: Returning 'error' directly in the catch block may break the expected return structure in getStaticProps. Consider returning a consistent object (e.g., { glossaryTerms: [] }) or implementing proper error handling to align with the expected format.
```suggestion
    return {
      glossaryTerms: [],
      error: 'Failed to fetch glossary terms',
    };
```
created_at: 2025-04-22T10:03:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,310 @@
+import { Layout } from '../../components/layout';
+import { PrismicRichText } from '@prismicio/react';
+import { asText } from '@prismicio/client';
+import Link from 'next/link';
+import { Button, Card, LinkButton, Text } from '@weshipit/ui';
+import { linksApi } from '../api/links';
+import { GetStaticPaths, GetStaticProps } from 'next';
+import { GlossaryTerm, getAllGlossaryTerms } from '../api/glossary';
+import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
+import { client as prismicClient } from '../api/prismic';
+
+// Helper function to create slug from title
+function slugify(text: string): string {
+  return text
+    .toString()
+    .toLowerCase()
+    .trim()
+    .replace(/\s+/g, '-') // Replace spaces with -
+    .replace(/&/g, '-and-') // Replace & with 'and'
+    .replace(/[^\w\-]+/g, '') // Remove all non-word characters except dashes
+    .replace(/\-\-+/g, '-'); // Replace multiple - with single -
+}
+
+interface GlossaryTermPageProps {
+  term: GlossaryTerm;
+  previousTerm: GlossaryTerm | null;
+  nextTerm: GlossaryTerm | null;
+  relatedArticles: any[];
+  termsByTitle: Record<string, GlossaryTerm>;
+}
+
+export const getStaticPaths: GetStaticPaths = async () => {
+  const { glossaryTerms } = await getAllGlossaryTerms();
+
+  const paths = glossaryTerms
+    .filter((term) => term.data.title)
+    .map((term) => ({
+      params: { uid: slugify(term.data.title) },
+    }));
+
+  return {
+    paths,
+    fallback: 'blocking',
+  };
+};
+
+export const getStaticProps: GetStaticProps = async ({ params }) => {
+  const slug = params?.uid as string;
+  if (!slug) {
+    return { notFound: true };
+  }
+
+  // Get all glossary terms
+  const { glossaryTerms } = await getAllGlossaryTerms();
+
+  // Find the term that matches the slug
+  const term = glossaryTerms.find((term) => slugify(term.data.title) === slug);
+
+  if (!term) {
+    return { notFound: true };
+  }
+
+  // Sort terms alphabetically for prev/next navigation
+  const sortedTerms = [...glossaryTerms].sort((a, b) =>
+    a.data.title.localeCompare(b.data.title),
+  );
+
+  const currentIndex = sortedTerms.findIndex(
+    (t) => slugify(t.data.title) === slug,
+  );
+
+  const previousTerm = currentIndex > 0 ? sortedTerms[currentIndex - 1] : null;
+  const nextTerm =
+    currentIndex < sortedTerms.length - 1
+      ? sortedTerms[currentIndex + 1]
+      : null;
+
+  // Create a map of terms by title for quick lookup
+  const termsByTitle: Record<string, GlossaryTerm> = {};
+  glossaryTerms.forEach((term) => {
+    if (term.data.title) {
+      termsByTitle[term.data.title] = term as unknown as GlossaryTerm;
+    }
+  });
+
+  // Fetch related articles from Prismic
+  let relatedArticles: any[] = [];
+  if (term.data.related_articles && term.data.related_articles.length > 0) {
+    try {
+      const articlePromises = term.data.related_articles.map(
+        async (article: any) => {
+          if (article.article && article.article.id) {
+            return await prismicClient.getByID(article.article.id);
+          }
+          return null;
+        },
+      );
+
+      const articles = await Promise.all(articlePromises);
+      relatedArticles = articles.filter(Boolean);
+    } catch (error) {
+      console.error('Error fetching related articles', error);
+    }
+  }
+
+  return {
+    props: {
+      term,
+      previousTerm,
+      nextTerm,
+      relatedArticles,
+      termsByTitle,
+    },
+    revalidate: 60,
+  };
+};
+
+export default function GlossaryTermPage({
+  term,
+  previousTerm,
+  nextTerm,
+  relatedArticles,
+  termsByTitle,
+}: GlossaryTermPageProps) {
+  if (!term) {
+    return null;
+  }
+
+  return (
+    <Layout
+      seoTitle={`${term.data.title} | React Native Glossary | weshipit.today`}
+      seoDescription={`Learn about ${term.data.title} in React Native development.`}
+      ogImageTitle={`${term.data.title} | React Native Glossary`}
+      withHeader
+      withFooter
+      withContainer
+    >
+      <article className="py-12">
+        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
+          <div className="mb-8">
+            <Link
+              href="/react-native-glossary"
+              className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 flex items-center"
+            >
+              <ChevronLeftIcon className="h-4 w-4 mr-1" />
+              Back to Glossary
+            </Link>
+          </div>
+
+          <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
+            <section className="lg:col-span-2">
+              <h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-6">
+                {term.data.title}
+              </h1>
+
+              <div className="prose dark:prose-invert max-w-none">
+                <PrismicRichText field={term.data.description} />
+              </div>
+
+              {term.data.related_to && term.data.related_to.length > 0 && (
+                <div className="mt-8">
+                  <h2 className="text-xl font-medium text-gray-900 dark:text-white mb-4">
+                    Related Terms
+                  </h2>
+                  <div className="flex flex-wrap gap-2">
+                    {term.data.related_to.map((related: any, index: number) => {
+                      // Get the text of the related term
+                      const relatedText = asText(related);
+                      // Look up the term in our map of terms by title
+                      const matchingTerm = relatedText
+                        ? termsByTitle[relatedText]
+                        : undefined;
+
+                      return (
+                        <span
+                          key={index}
+                          className="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-0.5 text-sm font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-200"
+                        >
+                          {matchingTerm ? (
+                            <Link
+                              href={`/react-native-glossary/${slugify(matchingTerm.data.title)}`}
+                              className="hover:text-blue-600 dark:hover:text-blue-400"
+                            >
+                              {relatedText}
+                            </Link>
+                          ) : (
+                            relatedText
+                          )}
+                        </span>
+                      );
+                    })}
+                  </div>
+                </div>
+              )}
+
+              {relatedArticles && relatedArticles.length > 0 && (
+                <div className="mt-12">
+                  <h2 className="text-xl font-medium text-gray-900 dark:text-white mb-4">
+                    Relevant Articles
+                  </h2>
+                  <div className="space-y-4">
+                    {relatedArticles.map((article) => (
+                      <div
+                        key={article.id}
+                        className="border border-gray-200 dark:border-gray-700 rounded-lg p-4"
+                      >
+                        <h3 className="text-lg font-medium text-gray-900 dark:text-white">
+                          <Link
+                            href={`/blog/${article.uid}`}
+                            className="hover:text-blue-600 dark:hover:text-blue-400"
+                          >
+                            {article.data.title}
+                          </Link>
+                        </h3>
+                        {article.data.excerpt && (
+                          <p className="mt-2 text-gray-500 dark:text-gray-400">
+                            {article.data.excerpt}
+                          </p>
+                        )}
+                      </div>
+                    ))}
+                  </div>
+                </div>
+              )}
+
+              {/* Previous / Next Navigation */}
+              <div className="mt-20 border-t border-gray-200 dark:border-gray-700 pt-8 flex justify-between">
+                <div>
+                  {previousTerm && (
+                    <LinkButton
+                      href={`/react-native-glossary/${slugify(previousTerm.data.title)}`}
+                      size="lg"
+                      variant="ghost"
+                      className="flex items-center"
+                    >
+                      <ChevronLeftIcon className="h-4 w-4 mr-2" />
+                      {previousTerm.data.title}
+                    </LinkButton>
+                  )}
+                </div>
+                <div>
+                  {nextTerm && (
+                    <LinkButton
+                      href={`/react-native-glossary/${slugify(nextTerm.data.title)}`}
+                      size="lg"
+                      variant="ghost"
+                      className="flex items-center"
+                    >
+                      {nextTerm.data.title}
+                      <ChevronRightIcon className="h-4 w-4 ml-2" />
+                    </LinkButton>
+                  )}
+                </div>
+              </div>
+            </section>
+
+            {/* Call to Action Card */}
+            <aside className="sticky top-8 h-fit lg:col-span-1">
path: apps/weshipit/pages/react-native-glossary/[uid].tsx
user: login: flexbox
body: do not dupplicate
created_at: 2025-04-23T12:29:36Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,310 @@
+import { Layout } from '../../components/layout';
+import { PrismicRichText } from '@prismicio/react';
+import { asText } from '@prismicio/client';
+import Link from 'next/link';
+import { Button, Card, LinkButton, Text } from '@weshipit/ui';
+import { linksApi } from '../api/links';
+import { GetStaticPaths, GetStaticProps } from 'next';
+import { GlossaryTerm, getAllGlossaryTerms } from '../api/glossary';
+import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
+import { client as prismicClient } from '../api/prismic';
+
+// Helper function to create slug from title
+function slugify(text: string): string {
path: apps/weshipit/pages/react-native-glossary/[uid].tsx
user: login: flexbox
body: do not dupplicate
created_at: 2025-04-23T12:30:56Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,199 @@
+import { Layout } from '../../components/layout';
+import { GlossaryTerm, getAllGlossaryTerms } from '../api/glossary';
+import { PrismicRichText } from '@prismicio/react';
+import { asText } from '@prismicio/client';
+import Link from 'next/link';
+import { Card, LinkButton, Prose, Text } from '@weshipit/ui';
+import { useEffect, useState } from 'react';
+import { linksApi } from '../api/links';
+
+// Helper function to create slug from title
+function slugify(text: string): string {
+  return text
+    .toString()
+    .toLowerCase()
+    .trim()
+    .replace(/\s+/g, '-') // Replace spaces with -
+    .replace(/&/g, '-and-') // Replace & with 'and'
+    .replace(/[^\w\-]+/g, '') // Remove all non-word characters except dashes
+    .replace(/\-\-+/g, '-'); // Replace multiple - with single -
+}
+
+interface GlossaryPageProps {
+  glossaryTerms: GlossaryTerm[];
+}
+
+export async function getStaticProps() {
+  const { glossaryTerms } = await getAllGlossaryTerms();
+
+  // Group terms by their first letter
+  const groupedTerms: { [key: string]: GlossaryTerm[] } = {};
+  glossaryTerms.forEach((term) => {
+    if (!term.data.title) return;
+
+    const firstLetter = term.data.title.charAt(0).toUpperCase();
+    const category = /^[0-9]/.test(firstLetter) ? '#' : firstLetter;
+
+    if (!groupedTerms[category]) {
+      groupedTerms[category] = [];
+    }
+
+    groupedTerms[category].push(term as unknown as GlossaryTerm);
+  });
+
+  return {
+    props: {
+      groupedTerms,
+    },
+  };
+}
+
+export default function ReactNativeGlossary({
+  groupedTerms,
+}: GlossaryPageProps & { groupedTerms: { [key: string]: GlossaryTerm[] } }) {
+  const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
+  const [activeLetters, setActiveLetters] = useState<string[]>([]);
+
+  useEffect(() => {
+    // Set active letters based on available terms
+    setActiveLetters(Object.keys(groupedTerms));
+  }, [groupedTerms]);
+
+  return (
+    <Layout
+      seoTitle="React Native Glossary | weshipit.today"
+      seoDescription="A comprehensive glossary of React Native terms and concepts to help you better understand React Native development."
+      ogImageTitle="React Native Glossary"
+      withHeader
+      withFooter
+      withContainer
+    >
+      <article className="py-12">
+        <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
+          <header className="lg:text-center">
+            <h1 className="mt-2 text-3xl font-bold leading-8 tracking-tight text-gray-900 dark:text-white sm:text-4xl">
+              React Native Glossary
+            </h1>
+            <p className="mt-4 max-w-2xl text-xl text-gray-500 dark:text-gray-400 lg:mx-auto">
+              A glossary of all the terms around React Native development.
+            </p>
+          </header>
+
+          {/* Alphabet Navigation */}
+          <nav className="mt-8 flex flex-wrap justify-center gap-2 sm:gap-4">
+            {alphabet.map((letter) => {
+              const isActive = activeLetters.includes(letter);
+              return (
+                <Link
+                  key={letter}
+                  href={isActive ? `#${letter}` : '#'}
+                  className={`inline-flex h-10 w-10 items-center justify-center rounded-md text-lg font-medium shadow-sm
+                    ${
+                      isActive
+                        ? 'bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 dark:bg-blue-700 dark:text-white dark:hover:bg-blue-800 dark:focus-visible:outline-blue-700'
+                        : 'border border-gray-300 bg-white text-gray-500 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700'
+                    }`}
+                  aria-disabled={!isActive}
+                >
+                  {letter}
+                </Link>
+              );
+            })}
+          </nav>
+
+          <div className="mt-12 grid grid-cols-1 gap-8 lg:grid-cols-3">
+            <section className="lg:col-span-2">
+              {activeLetters.sort().map((letter) => (
+                <div key={letter} id={letter} className="mb-12">
+                  <h2 className="mb-6 text-4xl font-bold text-gray-900 dark:text-white">
+                    {letter}
+                  </h2>
+
+                  <dl className="space-y-8">
+                    {groupedTerms[letter]?.map((item) => (
+                      <div
+                        key={item.id}
+                        className="relative border-b border-gray-200 dark:border-gray-700 pb-6"
+                      >
+                        <dt>
+                          <h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-white">
+                            <Link
+                              href={`/react-native-glossary/${slugify(item.data.title)}`}
+                              className="hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer"
+                            >
+                              {item.data.title}
+                            </Link>
+                          </h3>
+                        </dt>
+                        <dd className="mt-2 text-base text-gray-500 dark:text-gray-400">
+                          <PrismicRichText field={item.data.description} />
+                        </dd>
+                        {item.data.related_to &&
+                          item.data.related_to.length > 0 && (
+                            <div className="mt-4">
+                              <span className="text-sm font-medium text-gray-500 dark:text-gray-400">
+                                Related to:
+                              </span>
+                              <div className="mt-1 flex flex-wrap gap-2">
+                                {item.data.related_to.map(
+                                  (related: any, index: number) => (
+                                    <span
+                                      key={index}
+                                      className="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-0.5 text-sm font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-200"
+                                    >
+                                      {asText(related)}
+                                    </span>
+                                  ),
+                                )}
+                              </div>
+                            </div>
+                          )}
+                      </div>
+                    ))}
+                  </dl>
+                </div>
+              ))}
+            </section>
+
+            {/* Call to Action Card */}
+            <aside className="sticky top-8 h-fit lg:col-span-1">
path: apps/weshipit/pages/react-native-glossary/index.tsx
user: login: flexbox
body: Create a comoppent
created_at: 2025-04-23T12:31:17Z
author_association: OWNER

PR comments:
user: login: vercel[bot]
created_at: 2025-04-18T14:10:54Z
body: [vc]: #bRGYq2f2ROgGCMWURyYKGgem4eDRmbs18SwWZa9d8nY=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ3ZXNoaXBpdC10b2RheSIsInJvb3REaXJlY3RvcnkiOm51bGwsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ3ZXNoaXBpdC10b2RheS1naXQtZ2xvc3NhcnktZGF2aWQtbGV1bGlldHRlcy1wcm9qZWN0cy52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9kYXZpZC1sZXVsaWV0dGVzLXByb2plY3RzL3dlc2hpcGl0LXRvZGF5L0R1Vm5ZOUZoWnJ2cGRWYXB6TkJLOWs3aDdnVm0iLCJwcmV2aWV3VXJsIjoid2VzaGlwaXQtdG9kYXktZ2l0LWdsb3NzYXJ5LWRhdmlkLWxldWxpZXR0ZXMtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9LHsibmFtZSI6ImRlc2lnbi13ZXNoaXBpdC10b2RheSIsInJvb3REaXJlY3RvcnkiOm51bGwsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJkZXNpZ24td2VzaGlwaXQtdG9kYXktZ2l0LWdsb3NzYXJ5LWRhdmlkLWxldWxpZXR0ZXMtcHJvamVjdHMudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vZGF2aWQtbGV1bGlldHRlcy1wcm9qZWN0cy9kZXNpZ24td2VzaGlwaXQtdG9kYXkvQzMyU1N3TFdEU0ZQRlM3a3cyaTNKZ0tEdjRrRyIsInByZXZpZXdVcmwiOiJkZXNpZ24td2VzaGlwaXQtdG9kYXktZ2l0LWdsb3NzYXJ5LWRhdmlkLWxldWxpZXR0ZXMtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **design-weshipit-today** | ✅ Ready ([Inspect](https://vercel.com/david-leuliettes-projects/design-weshipit-today/C32SSwLWDSFPFS7kw2i3JgKDv4kG)) | [Visit Preview](https://design-weshipit-today-git-glossary-david-leuliettes-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/design-weshipit-today-git-glossary-david-leuliettes-projects.vercel.app?via=pr-comment-feedback-link) | Apr 24, 2025 8:09am |
| **weshipit-today** | ✅ Ready ([Inspect](https://vercel.com/david-leuliettes-projects/weshipit-today/DuVnY9FhZrvpdVapzNBK9k7h7gVm)) | [Visit Preview](https://weshipit-today-git-glossary-david-leuliettes-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/weshipit-today-git-glossary-david-leuliettes-projects.vercel.app?via=pr-comment-feedback-link) | Apr 24, 2025 8:09am |



author_association: NONE
user: login: flexbox
created_at: 2025-04-18T14:49:00Z
body: I added the `glossary` type for the data on prismic

![CleanShot 2025-04-18 at 16 50 12](https://github.com/user-attachments/assets/a0367dda-37a9-463b-9ca2-14517d24be28)


@MatthysDev Can you connect the data to the api 🙏 
you can keep the JSON data as fixture to create a story file 
author_association: OWNER
user: login: MatthysDev
created_at: 2025-04-22T16:22:57Z
body: <img width="656" alt="image" src="https://github.com/user-attachments/assets/ae718e28-d13a-49de-8679-ba075f02a0c4" />
😔
author_association: COLLABORATOR
user: login: MatthysDev
created_at: 2025-04-22T18:39:59Z
body: Ready for review / merge @flexbox 
author_association: COLLABORATOR