Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/teotimepacreau/mairies-sveltekit

User & Admin friendly fullstack website buil with SvelteKit
https://github.com/teotimepacreau/mairies-sveltekit

fastify shadc shadcn-ui sveltekit tailwind

Last synced: about 1 month ago
JSON representation

User & Admin friendly fullstack website buil with SvelteKit

Awesome Lists containing this project

README

        

# "Site de Mairie" : my fullstack project with SvelteKit frontend + Fastify backend

https://github.com/teotimepacreau/Mairies-Sveltekit/assets/95431443/f5b7c952-d9f2-4851-8484-ae6b4b984012

# 🛠️
Frontend : HTML, CSS, Tailwind, JS, SvelteKit
Backend: TinaCMS, Pagefind, Fastify

# Functionalities
- user-friendly administration to create/update/delete every page on the website
- contact form automatically sending an email containing form to the admin
- inner search function
- articles pagination
- smooth page transitions

# What I learned :
- using Svelte and SvelteKit
- implement a headless CMS in order to allow non tech users to admin the website
- querying a GraphQL API
- using Tailwind and work with an existing Design System
- splitting recursive elements in Svelte components
- customizing Svelte to generate a static website, and allowing also SSR for certain pages
- applying shared layouts to pages through Svelte, escaping layouts
- knowing when to use `+page.js` rather than `+page.server.js` and contrary
- converting Markdown files to HTML through Svelte

# In-depth details of the project :
## Progressive enhancement on hamburger menu
In order to not require JS to open and close the hamburger menu, I used the Popover API. It's a new HTML feature.

```

```

`/frontend/src/components/MobileNav.svelte`
## Allowing non-tech admins to manage the whole website easily
I wanted to allow non-tech admins to create/update/delete articles but also be capable to create/update/delete all pages of the website.
I choosed a headless Git based CMS in order to don't have the hussle to manage a Database and prioritize simplicity : [TinaCMS](https://tina.io/).
`/frontend/tina/config.js`

https://github.com/teotimepacreau/Mairies-Sveltekit/assets/95431443/c4887666-0eee-49cb-8ed6-8b6094fd689a

1. First defining a schema for the data with 2 collections : article (for news) and pages (for general page content), then defining each field with data types and requirements.
```
schema: {
collections: [
{
name: "article",
label: "Articles",
path: "src/articles",
fields: [
{
type: "string",
name: "titre",
label: "Titre",
isTitle: true,
required: true,
},
{
type: "string",
name: "desc",
label: "Description",
required: true,
},
{
type: "datetime",
name: "date",
label: "Date",
required: true,
},
{
type: "image",
name: "image",
label: "Image",
},
{
type: "string",
name: "imagealt",
label: "Description de l'image",
},
{
type: "rich-text",
name: "body",
label: "Corps de texte",
isBody: true, //⚠️ bien penser à mettre isBody: true au champ dont on souhaite qu’il souhaite render non pas en frontmatter mais bien en corps de texte markdown
},
],
},
{
name: "pages",
label: "Pages",
path: "src/pages",
fields: [
{
type: "string",
name: "titre",
label: "Titre",
isTitle: true,
required: true,
},
{
type: "string",
label: "Catégorie",
name: "categorie",
list: true,
required: true,
options: [
{
label: "Mairie",
value: "mairie",
},
{
label: "Vie Locale",
value: "vie locale",
},
{
label: "DĂ©marches",
value: "demarches",
}
],
},
{
type: "string",
label: "IcĂ´ne",
name: "emoji",
description: "Emoji qui servira d'icĂ´ne dans le menu de navigation",
required: true,
},
{
type: "rich-text",
name: "contenu",
label: "Contenu",
required: true,
isBody: true, //bien penser à mettre isBody: true au champ dont on souhaite qu’il souhaite render non pas en frontmatter mais bien en corps de texte markdown
},
],
},
],
},
```

Result is having a dedicated admin page, and pages for each collection (Articles, Pages)

2. Querying the generated GraphQL API in Svelte files in order to get data
```
import { client } from "@tina/__generated__/client";
async function fillArrayOfNavLinks() {
const result = await client.queries.pagesConnection();
try {
const {
data: {
pagesConnection: { edges },
},
} = result;
arrayOfNavLinks = edges;
return arrayOfNavLinks;
} catch (e) {
console.error(500, "Could not find articles on the server");
}
}
```

## Articles generation
1. Markdown files are processed through MDSVEX in order to generate HTML pages.
2. Accessing articles through the SvelteKit `load` native function in `/frontend/src/routes/(content)/actualites/[slug]/+page.js` : if the slug matches then content and metadata of the articles are passed to the `+page.svelte` through `data` variable. Then capturing `data.meta.titre`, `data.meta.image`... and rendering body of the article with ``

## Articles pagination through Shadcn-UI
I wanted to limit displayed articles to 3, and then needed a Pagination item to see hidden articles.
I used [Shadcn-Svelte](https://www.shadcn-svelte.com/) a UI component library.

https://github.com/teotimepacreau/Mairies-Sveltekit/assets/95431443/a3ccc618-21f2-4f73-b572-18df9925e156

`frontend/src/components/ActuItemsListAndPagination.svelte`
```
import * as Pagination from "@sveltecomponents/pagination";

// IIII. RENDRE CHAQUE BTN DE PAGINATION INTERACTIF POUR METTRE A JOUR LA VARIABLE currentPage puis appeler la fonction getSubsetOfArticlesForPagination() pour mettre Ă  jour le subset
onMount(async () => {
await getSubsetOfArticlesForPagination(); //la fonction de création du subset est appelée pour la 1ère fois ici
// IIII.1 intéractivité des btns n° de pa&0ge
let paginationNumberButtons = document.querySelectorAll(
"[data-melt-pagination-page]"
);
paginationNumberButtons.forEach((button) => {
button.addEventListener("click", () => {
currentPage = button.dataset.value;
getSubsetOfArticlesForPagination();
});
});

// IIII.2 intéractivité du btn "suivant"
let paginationNextButton = document.querySelector(
"[data-melt-pagination-next]"
);
paginationNextButton.addEventListener("click", () => {
currentPage++;
getSubsetOfArticlesForPagination();
});

// IIII.3 intéractivité du btn "précédent"
let paginationPrevButton = document.querySelector(
"[data-melt-pagination-prev]"
);
paginationPrevButton.addEventListener("click", () => {
currentPage--;
getSubsetOfArticlesForPagination();
});
});






{#each pages as page (page.key)}
{#if page.type === "ellipsis"}



{:else}


{page.value}


{/if}
{/each}





```
## Inner search function
Website search function is powered with [Pagefind](pagefind.app). It builds an index for static pages, and works only after the `build` because it analysis all HTML pages. It works only for static websites.

https://github.com/teotimepacreau/Mairies-Sveltekit/assets/95431443/ced8b1c9-0b45-46fa-b89f-8f046739b489

1. Installed Pagefind through NPM
```
npx pagefind --site "public"
```
2. I'm not using the pre-built search UI provided by Pagefind but rather accessing directly the Pagefind API
`/frontend/src/components/SearchDialog.svelte`
```

let query = "";
const handleSearch = async () => {
const pagefind = await import("/pagefind/pagefind.js");//appelle l'objet pagefind
const r = await pagefind.search(query);//tape l'API "search" de Pagefind (on fait remonter la query de l'utilisateur en string via bind:value={query})

console.log(r);//donne un array avec 4 objets parmis lesquels l'objet "results"
for (const result of r.results) {
console.log(await result.data());//obligé d'await result.data pour bien avoir les résultats de la query
}
};

Rechercher
handleSearch()} bind:value={query} type="text"
name="search" id="search" placeholder="Rechercher"/>

```

Then displaying the resuts via VanillaJS
3. I select what pages are searchable thanks to the `data-pagefind-body` attribute
```

{{ content | safe }}

```
4. `package.json` script to see pagefind functioning
```
"postbuildservepagefind": "tinacms build && vite build && pagefind --site build --serve"
```

## Contact form & email notification
Toast notification confirms if form is successully sent and received by the server. If form is not successfully received server-side, a toast notification informs the user. Then an email is sent to the admin's mail containing the form

https://github.com/teotimepacreau/Mairies-Sveltekit/assets/95431443/33b64b90-ea8c-4a0c-85f7-18804d375678

1. Set up a backend server through Fastify to receive form submission
`backend/src/server.js`
```
import fastify from "fastify";
import cors from "@fastify/cors";

const app = fastify();

// CORS
await app.register(cors, {
origin: "*",//to modify before prod to not allow every origin
methods: "GET, POST",
});

app.post("/api", async (req, res) => {
//it's the handler function
try {
let receivedForm = {
nom: req.body.nom,
prenom: req.body.prenom,
telephone: req.body.telephone,
email: req.body.email,
messagecontent: req.body.messagecontent,
};
console.log(receivedForm);

let msg = {
to: '[email protected]',
from: process.env.FROM_EMAIL,
subject: 'Demande de contact - site mairie',
html: `${receivedForm.prenom}${receivedForm.nom}${receivedForm.email}${receivedForm.telephone}

${receivedForm.messagecontent}

`,
};
}
})

//fonction qui permet de démarrer notre serveur
const start = async () => {
try {
await app.listen({ port: 3000 });
} catch (err) {
console.error(err);
process.exit(1); //permet de finir le processus en cas d'erreur avec le code erreur 1
}
};
// appel de la fonction pour démarrer serveur
start();
```
2. Set up a mail sender on backend server to transfer the form to the admin's mail
added to `backend/src/server.js`
```
import fastify from "fastify";
import cors from "@fastify/cors";
import sgMail from "@sendgrid/mail"

// SECRET ENREGISTRE VIA NODE20.0 5node package.json : --env-file=.env
sgMail.setApiKey(process.env.SENDGRID_API_KEY);

const app = fastify();

// ROUTES

app.get("/", async (req, res) => {
res.send({ message: "Hello from the route handler!" });
});

app.post("/api", async (req, res) => {
//it's the handler function
try {
let receivedForm = {
nom: req.body.nom,
prenom: req.body.prenom,
telephone: req.body.telephone,
email: req.body.email,
messagecontent: req.body.messagecontent,
};
console.log(receivedForm);

let msg = {
to: '[email protected]',
from: process.env.FROM_EMAIL,
subject: 'Demande de contact - site mairie',
html: `${receivedForm.prenom}${receivedForm.nom}${receivedForm.email}${receivedForm.telephone}

${receivedForm.messagecontent}

`,
};

(async () => {
try {
await sgMail.send(msg);
} catch (error) {
console.error(error);

if (error.response) {
console.error(error.response.body)
}
}
})();

res
.status(201)
.send({ confmessage: "Form received on backend with success" });
} catch (error) {
res
.status(500)
.send({
errMessage:
"Erreur côté serveur suite à la soumission de votre formulaire",
});
}
});
```

## Layouts
1. SvelteKit layout logic to apply style to all subpages with `+layout.svelte`
2. Create shared layout only within a subfolder but without affecting url thanks to `(content)` : `/frontend/src/routes/(content)/+layout.svelte`
## View Transitions
Using View Transitions API to create seamless navigation with the native fade-in animation
`/frontend/src/routes/+layout.svelte`
```
import { onNavigate } from "$app/navigation";

onNavigate((navigation) => {
if (!document.startViewTransition) return;

return new Promise((resolve) => {
document.startViewTransition(async () => {
resolve();
await navigation.complete;
});
});
});
```
## Components
Created several components from scratch :
1. Generate dynamically list of actu items by making a request to the CMS and paginate it (pagination is from Shadcn pre-made component) `/frontend/src/components/ActuItemsListAndPagination.svelte`
2. Dynamic Breadcrumbs adapting path and elements on each page `/frontend/src/components/Breadcrumb.svelte`
3. Generating navigation links by making a request to the CMS and ordering nav elements following their category `/frontend/src/components/Nav.svelte`
4. Search Button opening a `dialog` `/frontend/src/components/SearchButton.svelte`
5. Search Dialog allowing user to input the searched elements `/frontend/src/components/SearchDialog.svelte`

## Static Site Generation
I wanted the website to be as performant as possible so rendering the major part of the website as Static seemed important.
1. `/frontend/svelte.config.js`
```
import adapter from '@sveltejs/adapter-static'
const config = {
kit: {
prerender: {
crawl: true,
},
}
}
```
2. `/frontend/src/routes/.layout.js`
```
export const prerender = true
```

# Ops
Frontend deployed on Vercel
Backend deployed on fly.io