added full CRUD capabilities

This commit is contained in:
Donavon McDowell 2024-12-11 14:57:00 -07:00
parent 06fd32caa9
commit 08b58ee729
10 changed files with 473 additions and 131 deletions

View File

@ -42,7 +42,7 @@ app.use((err, req, res, next) => {
// Create a new document // Create a new document
app.post('/documents', async (req, res) => { app.post('/documents', async (req, res) => {
const { title, body, tags } = req.body; const { title, body, tags, created_by } = req.body;
// Validate input // Validate input
if (!title || !body) { if (!title || !body) {
@ -53,12 +53,15 @@ app.post('/documents', async (req, res) => {
return res.status(400).json({ error: 'Tags must be an array' }); return res.status(400).json({ error: 'Tags must be an array' });
} }
console.log(created_by);
try { try {
// Insert the document into the database // Insert the document into the database
const result = await db.collection('documents').insertOne({ const result = await db.collection('documents').insertOne({
title, title,
body, body,
tags, tags,
created_by,
created_at: new Date() created_at: new Date()
}); });

View File

@ -1,6 +0,0 @@
AUTH_SECRET="txvHsSGbEuQ6j3I3YZEyXkVJnPTP/s5vdK4+OJ8sxQo=" # Added by `npx auth`. Read more: https://cli.authjs.dev
AUTH_MICROSOFT_ENTRA_ID_ID='3cdfac60-e7fb-4648-89d3-67966c497d35'
AUTH_MICROSOFT_ENTRA_ID_SECRET='5Gi8Q~_pmDtvN3.Jwqt85kiI.uiyAAC7Z.4iFayY'
AUTH_MICROSOFT_ENTRA_ID_ISSUER='538b9b1c-23fa-4102-b36e-a4d83fc9c4c1'
POTATO='77863dd1-f8ab-4f6e-b94a-0f7a0340b320'

View File

@ -13,6 +13,11 @@
let element; let element;
let editor; let editor;
let initialHTML;
export function setHTML(html){
editor.commands.insertContent(html);
}
export function getHTML(){ export function getHTML(){
const html = editor.getHTML(); // Get the editor content as HTML const html = editor.getHTML(); // Get the editor content as HTML
@ -25,6 +30,7 @@
extensions: [ extensions: [
StarterKit, StarterKit,
Underline, Underline,
Image.configure({ block: true }),
Dropcursor, Dropcursor,
CodeBlock.configure({ CodeBlock.configure({
exitOnTripleEnter: true, exitOnTripleEnter: true,
@ -34,7 +40,6 @@
class: 'bg-gray-900 border-2 border-gray-500 rounded-xl p-2 text-white', class: 'bg-gray-900 border-2 border-gray-500 rounded-xl p-2 text-white',
}, },
}), }),
Image,
TextAlign.configure({ TextAlign.configure({
types: ['heading', 'paragraph'], types: ['heading', 'paragraph'],
}), }),
@ -79,7 +84,11 @@
}, },
}), }),
], ],
// place the cursor in the editor after initialization
autofocus: true,
// make the text editable (but thats the default anyway)
editable: true, editable: true,
// disable the loading of the default CSS (which is not much anyway)
injectCSS: false, injectCSS: false,
onTransaction: () => { onTransaction: () => {
// force re-render so `editor.isActive` works as expected // force re-render so `editor.isActive` works as expected
@ -90,6 +99,7 @@
class: 'prose-base m-3 focus:outline-none', class: 'prose-base m-3 focus:outline-none',
}, },
}, },
content: initialHTML,
}); });
}); });

View File

@ -1,12 +1,59 @@
<script lang="ts"> <script lang="ts">
import '../app.postcss'; import '../app.postcss';
import { signIn, signOut } from "@auth/sveltekit/client"
import { page } from "$app/stores"
let dropDown = false;
</script> </script>
<header class="bg-blue-700 flex flex-row p-4 place-content-between items-center font-roboto shadow-xl">
<a href="/home">
<img class="max-w-36 mr-5 ml-4 aspect-auto" src="/branding/mpe_logo.png" alt="MPE Logo">
</a>
<header class="bg-blue-700 flex flex-row p-4 place-content-between items-center"> <div class="relative dropdown-container">
<a href="/home"><img class="max-w-36 mr-5 ml-4 aspect-auto" src="/branding/mpe_logo.png" alt="MPE Logo"></a> <button class="rounded-3xl bg-blue-500 p-2 flex flex-row justify-center items-center p-1 hover:bg-blue-400"
<a href="/add" class="rounded-3xl bg-blue-500 text-white p-2 px-10 font-roboto shadow m-1 h-10">Add</a> on:click={() => dropDown = !dropDown}>
{#if $page.data.session}
{#if $page.data.session.user?.image}
<img
src={$page.data.session.user.image}
class="avatar rounded-full size-8"
alt="User Avatar"
/>
<i class="fa-solid fa-chevron-down ml-2 mr-2" style="color: #FFFFFF;"></i>
{/if}
{:else}
<p class="font-bold text-white ml-1">Not Signed In</p>
<i class="fa-solid fa-chevron-down ml-2 mr-2" style="color: #FFFFFF;"></i>
{/if}
</button>
{#if dropDown}
<!-- Dropdown menu -->
<div class="absolute right-0 mt-2 bg-white shadow-lg rounded-md p-4 z-50 text-nowrap">
{#if $page.data.session}
<span class="signedInText block mb-2 text-sm">
<small>Signed in as</small><br/>
<strong>{$page.data.session.user?.name ?? "User"}</strong>
</span>
<button
class="bg-blue-500 text-white px-4 py-2 rounded-3xl hover:bg-blue-400 text-nowrap w-full"
on:click={() => { signOut(); dropDown = false;}}>
Sign Out
</button>
{:else}
<span class="notSignedInText block mb-2">You are not signed in</span>
<button
class="bg-blue-500 text-white px-4 py-2 rounded-3xl hover:bg-blue-400 text-nowrap w-full"
on:click={() => { signIn('microsoft-entra-id'); dropDown = false;}}>
Sign In
</button>
{/if}
</div>
{/if}
</div>
</header> </header>
<slot /> <slot />

View File

@ -1,27 +0,0 @@
<script>
import { signIn, signOut } from "@auth/sveltekit/client"
import { page } from "$app/stores"
</script>
<h1>SvelteKit Auth Example</h1>
<div>
{#if $page.data.session}
{#if $page.data.session.user?.image}
<img
src={$page.data.session.user.image}
class="avatar"
alt="User Avatar"
/>
{/if}
<span class="signedInText">
<small>Signed in as</small><br />
<strong>{$page.data.session.user?.name ?? "User"}</strong>
<button on:click={() => signOut()}>Sign Out</button>
</span>
{:else}
<span class="notSignedInText">You are not signed in</span>
<div class="wrapper-form">
<button on:click={() => signIn('microsoft-entra-id')}>Sign In with M365</button>
</div>
{/if}
</div>

View File

@ -3,6 +3,7 @@
</svelte:head> </svelte:head>
<script> <script>
import { page } from "$app/stores"
import Tiptap from '$lib/components/Tiptap.svelte' import Tiptap from '$lib/components/Tiptap.svelte'
let editorRef; //This is our reference to the tiptap editor let editorRef; //This is our reference to the tiptap editor
@ -13,6 +14,9 @@
const url = "http://localhost:3000/documents"; const url = "http://localhost:3000/documents";
console.log("Title value:", title); console.log("Title value:", title);
let created_by = $page.data.session.user?.name ?? "User";
// Get the HTML from the tiptap editor // Get the HTML from the tiptap editor
if (editorRef) { if (editorRef) {
// Get the HTML content from the Tiptap editor // Get the HTML content from the Tiptap editor
@ -26,7 +30,8 @@
const payload = { const payload = {
title, title,
body, body,
tags tags,
created_by
}; };
console.log(payload); console.log(payload);
@ -72,11 +77,13 @@
} }
tagInput.value = ''; // Clear the input field tagInput.value = ''; // Clear the input field
} }
let dropMenu = false;
let visibility = 'Public';
</script> </script>
<div class="flex items-center justify-center h-full flex-col"> <div class="flex items-center justify-center h-full flex-col mb-10">
<div class="w-full flex-col justify-center w-11/12 md:w-8/12 xl:w-3/4 mt-20"> <form class="flex-col justify-center w-11/12 md:w-10/12 xl:w-8/12 mt-20">
<div class="flex flex-row w-full">
<input <input
type="text" type="text"
placeholder="Type your title here..." placeholder="Type your title here..."
@ -86,17 +93,41 @@
bind:value={title} bind:value={title}
required required
/> />
<!-- Dropdown Button -->
<div class="relative mb-1">
<button
class="bg-white text-blue-500 rounded-xl focus:outline-none flex flex-row justify-center items-center px-2 ml-1 font-roboto border-2 border-blue-500 h-full text-nowrap"
on:click={() => dropMenu = !dropMenu}>
{visibility}
<i class="fa-solid fa-chevron-down ml-1"></i>
</button>
<!-- Dropdown Menu -->
{#if dropMenu}
<div class="absolute right-0 mt-2 bg-white shadow-lg rounded-md z-50 text-nowrap">
<button
class="block w-full text-left px-8 py-2 hover:bg-gray-100 text-gray-700" on:click={() => { visibility = 'Public'; dropMenu = false;}}>
Public
</button>
<button
class="block w-full text-left px-8 py-2 hover:bg-gray-100 text-gray-700" on:click={() => { visibility = 'Private'; dropMenu = false;}}>
Private
</button>
</div>
{/if}
</div>
</div>
<Tiptap bind:this={editorRef}/> <!-- This is the text editor itself --> <Tiptap bind:this={editorRef}/> <!-- This is the text editor itself -->
<div class="w-full flex flex-col justify-between mt-1"> <!-- Bottom Container --> <div class="w-full flex flex-col justify-between mt-1"> <!-- Bottom Container -->
<div class="rounded-xl bg-white flex flex-col p-1"> <!-- This is where the tags will go --> <div class="rounded-xl bg-white flex flex-col p-1"> <!-- This is where the tags will go -->
<div class="w-full flex flex-row p-1"> <div class="w-full flex flex-row p-1">
<input type="text" name="tag" id="tag" placeholder="Type a tag..." class="w-full outline-none p-2 rounded-3xl "/> <input type="text" name="tag" id="tag" placeholder="Type a tag..." class="w-full outline-none p-2 rounded-3xl "/>
<button class="rounded-xl bg-blue-500 text-white shadow px-4 font-roboto" on:click={addTag}>Add</button> <button class="rounded-full bg-blue-500 text-white shadow px-4 font-roboto hover:bg-blue-400" on:click={addTag}>Add</button>
</div> </div>
<div class="p-2"> <div class="p-2">
{#each tags as tag, i} {#each tags as tag, i}
<button class="inline-flex rounded-xl p-1 px-3 items-center bg-blue-500 text-white space-x-2 mr-1 border-dashed border-blue-100 border-2" on:click={removeTag(tag)}> <button class="inline-flex rounded-xl p-1 px-3 items-center bg-blue-500 text-white space-x-2 mr-1 border-dashed border-blue-100 border-2 font-roboto" on:click={removeTag(tag)}>
<p>{tag}</p> <p>{tag}</p>
<i class="fa-solid fa-x fa-xs cursor-pointer"></i> <i class="fa-solid fa-x fa-xs cursor-pointer"></i>
</button> </button>
@ -104,8 +135,11 @@
</div> </div>
</div> </div>
<div class="flex flex-row w-full justify-end mt-1"> <div class="flex flex-row w-full justify-end mt-1">
<button class="rounded-3xl bg-blue-500 text-white p-2 px-10 font-roboto shadow" on:click={save}>Save</button> <button type="submit" class="rounded-full bg-blue-500 hover:bg-blue-400 text-white p-2 px-6 font-roboto shadow" on:click={save}>
</div> Save
<i class="fa-regular fa-floppy-disk fa-lg ml-2"></i>
</button>
</div> </div>
</div> </div>
</form>
</div> </div>

View File

@ -1,13 +1,15 @@
<script> <script lang="js">
import { page } from "$app/stores"
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let document = null; // To store fetched document data let document = null; // To store fetched document data
let error = null; // To store any error that occurs during fetch let error = null; // To store any error that occurs during fetch
let id;
// Fetch the data when the component mounts // Fetch the data when the component mounts
onMount(async () => { onMount(async () => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id'); // Get the `id` from the query string id = urlParams.get('id'); // Get the `id` from the query string
if (!id) { if (!id) {
error = 'ID is required'; error = 'ID is required';
@ -26,15 +28,108 @@
error = `Error: ${err.message}`; error = `Error: ${err.message}`;
} }
}); });
</script>
{#if error} let alertPopup = false;
<p>{error}</p> let agreeToDelete = false;
{:else if document} // Function to delete a document by ID
<div class="prose-base flex bg-white m-auto p-4 w-11/12 md:w-8/12 xl:w-3/4 leading-3"> async function deleteDocument(id) {
const url = `http://localhost:3000/documents/${id}`; // Update with your API base URL if different
// Show the confirmation popup
alertPopup = true;
// Wait until the user makes a decision
const interval = setInterval(() => {
if (agreeToDelete || !alertPopup) {
clearInterval(interval); // Stop checking once the decision is made
}
}, 100);
// Continue only if the user agrees
while (!agreeToDelete && alertPopup) {
await new Promise((resolve) => setTimeout(resolve, 100)); // Wait for decision
}
// Proceed with deletion if agreed
if (agreeToDelete) {
try {
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json();
console.error('Failed to delete document:', errorData.error);
alert(`Error: ${errorData.error}`);
return;
}
const result = await response.json();
console.log(result.message);
// Redirect after successful deletion
window.location.href = "/home";
} catch (error) {
console.error('Error deleting document:', error);
alert('An unexpected error occurred while deleting the document.');
}
}
// Reset popup and decision
alertPopup = false;
agreeToDelete = false;
}
</script>
{#if alertPopup}
<!-- Full-screen overlay to darken the background -->
<div
class="fixed inset-0 bg-gray-600 opacity-40 z-40"
></div>
<!-- Centered popup -->
<div
class="fixed bg-white rounded-xl p-6 shadow-lg text-center z-50"
style="top: 50%; left: 50%; transform: translate(-50%, -50%);"
>
<p class="mb-4 font-roboto">Do you agree to delete this document forever?</p>
<div class="flex justify-center space-x-4">
<button
class="bg-gray-200 text-black px-4 py-2 rounded-lg hover:bg-gray-300 font-roboto"
on:click={() => alertPopup = false}
>
Cancel
</button>
<button
class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 font-roboto"
on:click={() => agreeToDelete = true}
>
Delete
</button>
</div>
</div>
{/if}
{#if error}
<p>{error}</p>
{:else if document}
<div class="prose-base flex bg-white m-auto p-4 w-11/12 md:w-10/12 xl:w-8/12 leading-3 mt-6 mb-24 rounded-lg shadow">
<div class="prose-base w-full"> <div class="prose-base w-full">
<div class="flex flex-col justify-between"> <div class="flex flex-col justify-between">
<h1>{document.title}</h1> <div class="w-full flex flex-row justify-between">
<h1 class="text-2xl">{document.title}</h1>
{#if $page.data.session}
<div>
<a href="/edit?id={id}" class="bg-yellow-500 hover:bg-blue-400 border-double border-blue-100 border-2 p-3 rounded-full px-4 text-white font-roboto"><i class="fa-solid fa-pencil fa-lg"></i></a>
<button class="bg-red-500 hover:bg-blue-400 border-double border-blue-100 border-2 p-3 rounded-full px-4 text-white font-roboto" on:click={() => { deleteDocument(documentId)}}><i class="fa-solid fa-trash fa-lg"></i></button>
</div>
{/if}
</div>
<div class="leading-3">
<div class="flex flex-row h-8"> <div class="flex flex-row h-8">
{#each document.tags as tag} {#each document.tags as tag}
<div class="inline-flex rounded-xl px-3 items-center bg-blue-500 text-white space-x-2 mr-1 border-dashed border-blue-100 border-2"> <div class="inline-flex rounded-xl px-3 items-center bg-blue-500 text-white space-x-2 mr-1 border-dashed border-blue-100 border-2">
@ -42,19 +137,26 @@
</div> </div>
{/each} {/each}
</div> </div>
{#if document.author} <div class="flex flex-row justify-between">
<p>By: {document.author}</p> <div>
{#if document.created_by}
<p class="font-semibold">By: {document.created_by}</p>
{/if} {/if}
</div>
<div class="">
{#if document.created_at} {#if document.created_at}
<p>{new Date(document.created_at).toLocaleString()}</p> <p class="font-semibold">{new Date(document.created_at).toLocaleString()}</p>
{/if} {/if}
<hr class="w-full"> </div>
</div>
<hr class="w-full m-0">
</div>
</div> </div>
{@html document.body} {@html document.body}
</div> </div>
</div> </div>
{:else} {:else}
<p>Loading...</p> <!-- Loading animation could go here -->
{/if} {/if}

View File

@ -0,0 +1,189 @@
<svelte:head>
<title>Add</title>
</svelte:head>
<script>
import { onMount, onDestroy } from 'svelte';
import { page } from "$app/stores"
import Tiptap from '$lib/components/Tiptap.svelte'
let editorRef ; //This is our reference to the tiptap editor
let title = "";
let document = null; // To store fetched document data
let error = null; // To store any error that occurs during fetch
let documentId;
// Function to fetch the document by ID
async function fetchDocumentById(id) {
try {
const response = await fetch(`http://localhost:3000/documents/${id}`);
if (response.ok) {
const fetchedDocument = await response.json();
document = fetchedDocument; // Update document data
console.log(document.body);
editorRef.setHTML(document.body); // Update the editor with the HTML
} else {
error = `Failed to fetch document: ${response.status}`;
}
} catch (err) {
error = `Error: ${err.message}`;
}
}
// Set up polling on mount
onMount(() => {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id'); // Get the `id` from the query string
documentId = id;
if (!id) {
error = 'ID is required';
return;
}
// Fetch the document immediately
fetchDocumentById(id);
});
async function save() {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id'); // Get the `id` from the query string
const url = "http://localhost:3000/documents/"+id;
console.log("Title value:", title);
let created_by = $page.data.session.user?.name ?? "User";
// Get the HTML from the tiptap editor
if (editorRef) {
// Get the HTML content from the Tiptap editor
const body = editorRef ? editorRef.getHTML() : "";
if (!title.trim() || !body.trim()) {
console.error("Title and body are required.");
return;
}
const payload = {
title,
body,
tags,
created_by
};
console.log(payload);
try {
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
// Redirect to /home after successful completion
window.location.href = "/home"; // Redirecting to /home
} catch (error) {
console.log(error);
}
}
}
let tags = [];
// Function to remove a tag
function removeTag(tag) {
tags = tags.filter(t => t !== tag); // Filter out the tag from the array
}
// Function to add a tag
function addTag() {
const tagInput = document.querySelector('#tag');
const tag = tagInput.value.trim(); // Get input value and trim whitespace
if (tag && !tags.includes(tag)) { // Ensure non-empty and no duplicates
tags = [...tags, tag]; // Create a new array to trigger reactivity
}
tagInput.value = ''; // Clear the input field
}
let dropMenu = false;
let visibility = 'Public';
</script>
<div class="flex items-center justify-center h-full flex-col mb-10">
<form class="flex-col justify-center w-11/12 md:w-10/12 xl:w-8/12 mt-20">
<div class="flex flex-row w-full">
<input
type="text"
placeholder="Type your title here..."
name="title"
id="title"
class="rounded-xl w-full p-2 font-roboto mb-1 px-4 focus:outline-none"
bind:value={title}
required
/>
<!-- Dropdown Button -->
<div class="relative mb-1">
<button
class="bg-white text-blue-500 rounded-xl focus:outline-none flex flex-row justify-center items-center px-2 ml-1 font-roboto border-2 border-blue-500 h-full text-nowrap"
on:click={() => dropMenu = !dropMenu}>
{visibility}
<i class="fa-solid fa-chevron-down ml-1"></i>
</button>
<!-- Dropdown Menu -->
{#if dropMenu}
<div class="absolute right-0 mt-2 bg-white shadow-lg rounded-md z-50 text-nowrap">
<button
class="block w-full text-left px-8 py-2 hover:bg-gray-100 text-gray-700" on:click={() => { visibility = 'Public'; dropMenu = false;}}>
Public
</button>
<button
class="block w-full text-left px-8 py-2 hover:bg-gray-100 text-gray-700" on:click={() => { visibility = 'Private'; dropMenu = false;}}>
Private
</button>
</div>
{/if}
</div>
</div>
<Tiptap bind:this={editorRef}/> <!-- This is the text editor itself -->
<div class="w-full flex flex-col justify-between mt-1"> <!-- Bottom Container -->
<div class="rounded-xl bg-white flex flex-col p-1"> <!-- This is where the tags will go -->
<div class="w-full flex flex-row p-1">
<input type="text" name="tag" id="tag" placeholder="Type a tag..." class="w-full outline-none p-2 rounded-3xl "/>
<button class="rounded-full bg-blue-500 text-white shadow px-4 font-roboto hover:bg-blue-400" on:click={addTag}>Add</button>
</div>
<div class="p-2">
{#each tags as tag, i}
<button class="inline-flex rounded-xl p-1 px-3 items-center bg-blue-500 text-white space-x-2 mr-1 border-dashed border-blue-100 border-2 font-roboto" on:click={removeTag(tag)}>
<p>{tag}</p>
<i class="fa-solid fa-x fa-xs cursor-pointer"></i>
</button>
{/each}
</div>
</div>
<div class="flex flex-row w-full justify-end mt-1">
<button type="submit" class="rounded-full bg-blue-500 hover:bg-blue-400 text-white p-2 px-6 font-roboto shadow" on:click={save}>
Save
<i class="fa-regular fa-floppy-disk fa-lg ml-2"></i>
</button>
</div>
</div>
</form>
</div>

View File

@ -1,5 +1,6 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from "$app/stores"
let query = false; let query = false;
let searchQuery = ""; let searchQuery = "";
@ -72,6 +73,13 @@
</script> </script>
<div class="flex items-center justify-center h-full flex-col"> <div class="flex items-center justify-center h-full flex-col">
{#if $page.data.session}
<div class="flex flex-row justify-end w-full">
<a href="/add" class="bg-green-500 hover:bg-blue-400 border-double border-blue-100 border-2 p-3 rounded-full px-4 mr-4 mt-3 text-white font-roboto">
<i class="fa-solid fa-folder-plus fa-lg"></i>
</a>
</div>
{/if}
<div class="flex-col justify-center w-11/12 md:w-8/12 xl:w-1/2 max-w-7xl mt-20"> <div class="flex-col justify-center w-11/12 md:w-8/12 xl:w-1/2 max-w-7xl mt-20">
<!-- This is where the search box goes --> <!-- This is where the search box goes -->
<input <input
@ -118,6 +126,7 @@
<div class="bg-white rounded-2xl p-4 font-roboto border mt-1 mb-1 hover:bg-slate-200 truncate h-48 shadow-lg"> <div class="bg-white rounded-2xl p-4 font-roboto border mt-1 mb-1 hover:bg-slate-200 truncate h-48 shadow-lg">
<h3 class="text-lg font-semibold">{@html document.title}</h3> <h3 class="text-lg font-semibold">{@html document.title}</h3>
<p class="text-sm text-gray-600">{@html truncateBody(document.body)}</p> <p class="text-sm text-gray-600">{@html truncateBody(document.body)}</p>
</div> </div>
</a> </a>
{/each} {/each}

View File

@ -32,25 +32,6 @@
<button type="submit" class="flex w-full justify-center rounded-md bg-blue-500 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500">Sign in</button> <button type="submit" class="flex w-full justify-center rounded-md bg-blue-500 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500">Sign in</button>
</div> </div>
</form> </form>
{#if $page.data.session}
{#if $page.data.session.user?.image}
<img
src={$page.data.session.user.image}
class="avatar"
alt="User Avatar"
/>
{/if}
<span class="signedInText">
<small>Signed in as</small><br />
<strong>{$page.data.session.user?.name ?? "User"}</strong>
<button on:click={() => signOut()}>Sign Out</button>
</span>
{:else}
<span class="notSignedInText">You are not signed in</span>
<div class="wrapper-form">
<button on:click={() => signIn('microsoft-entra-id')}>Sign In with M365</button>
</div>
{/if}
<p class="mt-10 text-center text-sm/6 text-gray-500"> <p class="mt-10 text-center text-sm/6 text-gray-500">
Not a member? Not a member?