Skip to content

Commit 391ce92

Browse files
authored
feat: implement user profile menu (#111)
### Description <!-- Please add PR description (don't leave blank) - example: This PR [adds/removes/fixes/replaces] the [feature/bug/etc] --> This PR implements the `Profile` menu component with a menu items and logout functionality; Also integrated the component into few selected pages across the project... these are... - The Homepage - The Jargons Editor #### Changes Made - Introduced a new `Profile` component with a menu dropdown as an island created using react.js - Added a new `doLogout` action, which runs the task of removing the `jargondevToken` value from user cookies - Added a `/logout` page/route where the `doLogout` function is executed for logout functionality; this redirects the user to either the homepage or a value specified in the `return_to` param when the `/logout` route is called ### Related Issue <!-- Please prefix the issue number with Fixes/Resolves - example: Fixes #123 or Resolves #123 --> Fixes #71 ### Screenshots/Screencasts <!-- Please provide screenshots or video recording that demos your changes (especially if it's a visual change) --> [screencast-localhost_4321-2024_11_20-02_03_20.webm](https://github.com/user-attachments/assets/4fb14c2d-f867-488f-99d5-2901842403d5) ### Notes to Reviewer <!-- Please state here if you added a new npm packages, or any extra information that can help reviewer better review you changes --> NA
1 parent 374c519 commit 391ce92

5 files changed

Lines changed: 135 additions & 7 deletions

File tree

src/components/islands/profile.jsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { useState } from "react";
2+
3+
/**
4+
* Profile Menu Island
5+
* @param {{ isAuthed: boolean, userData: {}, authUrl: string }} props
6+
*/
7+
export default function Profile({ isAuthed, userData, authUrl }) {
8+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
9+
10+
// User is not logged in - not connected with GitHub
11+
if (!isAuthed) {
12+
return (
13+
<a href={authUrl} className="flex items-center justify-center px-2 py-1.5 md:px-3 md:py-2 text-sm md:text-base font-medium bg-black text-white border no-underline rounded-lg focus:outline-none hover:shadow-lg">
14+
<svg className="size-4 md:size-5 mr-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
15+
<path fillRule="evenodd" d="M10 .333A9.911 9.911 0 0 0 6.866 19.65c.5.092.678-.215.678-.477 0-.237-.01-1.017-.014-1.845-2.757.6-3.338-1.169-3.338-1.169a2.627 2.627 0 0 0-1.1-1.451c-.9-.615.07-.6.07-.6a2.084 2.084 0 0 1 1.518 1.021 2.11 2.11 0 0 0 2.884.823c.044-.503.268-.973.63-1.325-2.2-.25-4.516-1.1-4.516-4.9A3.832 3.832 0 0 1 4.7 7.068a3.56 3.56 0 0 1 .095-2.623s.832-.266 2.726 1.016a9.409 9.409 0 0 1 4.962 0c1.89-1.282 2.717-1.016 2.717-1.016.366.83.402 1.768.1 2.623a3.827 3.827 0 0 1 1.02 2.659c0 3.807-2.319 4.644-4.525 4.889a2.366 2.366 0 0 1 .673 1.834c0 1.326-.012 2.394-.012 2.72 0 .263.18.572.681.475A9.911 9.911 0 0 0 10 .333Z" clipRule="evenodd"></path>
16+
</svg> Connect
17+
</a>
18+
)
19+
}
20+
21+
return (
22+
<div className="relative">
23+
{/* Profile */}
24+
<button
25+
className={`${isDropdownOpen && "ring-4"} relative flex items-center justify-center size-10 hover:ring-4 ring-gray-200 overflow-hidden bg-transparent rounded-full transition-colors duration-700 cursor-pointer focus-visible:outline-none`}
26+
onClick={() => setIsDropdownOpen(prev => !prev)}
27+
>
28+
{/* User Avatar */}
29+
<img className="size-10 rounded-full" loading="lazy" src={userData.avatar_url} alt={userData.login} />
30+
31+
{/* Display Close Visual Cue */}
32+
{isDropdownOpen && (
33+
<div className="absolute flex items-center justify-center text-white backdrop-blur-sm size-full rounded-full">
34+
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
35+
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18 17.94 6M18 18 6.06 6"/>
36+
</svg>
37+
</div>
38+
)}
39+
</button>
40+
41+
{/* Dropdown */}
42+
<div className={`${!isDropdownOpen && "hidden"} z-50 absolute overflow-hidden mt-2 right-0 bg-white border text-sm divide-y divide-gray-100 rounded-lg shadow-lg min-w-64`}>
43+
<div className="flex items-center space-x-2 px-4 py-3 text-sm">
44+
{/* User Avatar */}
45+
<img className="size-10 rounded-full" loading="lazy" src={userData.avatar_url} alt={userData.login} />
46+
47+
{/* User Name & Login */}
48+
<div>
49+
<div className="line-clamp-1 break-all text-base">{userData.name || userData.login}</div>
50+
<div className="text-xs font-medium truncate">@{userData.login}</div>
51+
</div>
52+
</div>
53+
54+
{/* Menu Items */}
55+
<ul className="p-1">
56+
<li>
57+
<a href="/editor" className="no-underline flex items-center space-x-2 px-4 py-2 rounded-sm hover:bg-gray-100">
58+
<svg className="size-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
59+
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="m14.304 4.844 2.852 2.852M7 7H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-4.5m2.409-9.91a2.017 2.017 0 0 1 0 2.853l-6.844 6.844L8 14l.713-3.565 6.844-6.844a2.015 2.015 0 0 1 2.852 0Z"/>
60+
</svg>
61+
<span>
62+
Editor
63+
</span>
64+
</a>
65+
</li>
66+
</ul>
67+
68+
{/* Logout/Disconnect */}
69+
<div>
70+
<a href="/logout" className="no-underline flex items-center space-x-2 px-5 py-2 hover:bg-red-100 hover:text-red-900">
71+
<svg className="size-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
72+
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M20 12H8m12 0-4 4m4-4-4-4M9 4H7a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h2"/>
73+
</svg>
74+
<span>
75+
Disconnect
76+
</span>
77+
</a>
78+
</div>
79+
</div>
80+
</div>
81+
);
82+
}

src/lib/actions/do-logout.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Remove a GitHub Session - Logout of an OAuth Session
3+
* @param {import("astro").AstroGlobal} astroGlobal
4+
*/
5+
export default async function doLogout(astroGlobal) {
6+
const { cookies } = astroGlobal;
7+
8+
try {
9+
cookies.delete("jargonsdevToken");
10+
11+
return {
12+
isLoggedOut: true
13+
}
14+
} catch (error) {
15+
return {
16+
isLoggedOut: false
17+
}
18+
}
19+
}

src/pages/editor/index.astro

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import BaseLayout from "../../layouts/base.astro";
44
import doAuth from "../../lib/actions/do-auth.js";
55
import Navbar from "../../components/navbar.astro";
66
import { buildStatsUrl } from "../../lib/utils/index.js";
7+
import Profile from "../../components/islands/profile.jsx";
78
import { PROJECT_REPO_DETAILS } from "../../../constants.js";
89
import doContributionStats from "../../lib/actions/do-contribution-stats.js";
910
import ContributionCTA from "../../components/contribution-cta.astro";
1011
1112
const { url: { pathname }, redirect } = Astro;
1213
13-
const { isAuthed, authedData: userData } = await doAuth(Astro);
14+
const { isAuthed, authedData: userData, getAuthUrl } = await doAuth(Astro);
1415
if (!isAuthed) return redirect(`/login?return_to=${encodeURIComponent(pathname)}`);
1516
1617
const { newWords, editedWords, pendingWords } = await doContributionStats(Astro);
@@ -27,10 +28,14 @@ const totalWords = {
2728
pageTitle="Jargons Editor"
2829
class="flex flex-col w-full min-h-screen"
2930
>
30-
<!-- Navbar
31-
TODO: implement profile menu here, should carry logout functionality
32-
-->
33-
<Navbar returnNav={{ label: "Back to Dictionary", location: "../" }} />
31+
<Navbar returnNav={{ label: "Back to Dictionary", location: "../" }}>
32+
<Profile
33+
isAuthed={isAuthed}
34+
userData={userData}
35+
authUrl={getAuthUrl({path: "/editor"})}
36+
client:load
37+
/>
38+
</Navbar>
3439

3540
<main class="w-full max-w-screen-lg mx-auto flex flex-col grow p-5 space-y-3">
3641
<!-- Profile Section -->

src/pages/index.astro

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
---
22
import { getCollection } from "astro:content";
33
import BaseLayout from "../layouts/base.astro";
4+
import doAuth from "../lib/actions/do-auth.js";
45
import Search from "../components/islands/search.jsx";
6+
import Profile from "../components/islands/profile.jsx";
57
import RecentSearches from "../components/islands/recent-searches.jsx";
68
9+
const { isAuthed, authedData, getAuthUrl } = await doAuth(Astro);
10+
if (Astro.url.searchParams.get("code")) return Astro.redirect("/");
11+
712
const dictionary = await getCollection("dictionary");
813
---
914

@@ -12,9 +17,18 @@ const dictionary = await getCollection("dictionary");
1217
subtitle="Simplified Meaning & Definition to Technical Terms"
1318
class:list="min-h-screen flex flex-col"
1419
>
15-
<nav class="@container flex items-center justify-end px-5 md:px-6 py-4">
16-
<iframe src="https://ghbtns.com/github-btn.html?user=jargonsdev&repo=jargons.dev&type=watch&count=true&size=large" frameborder="0" scrolling="0" width="120" height="30" title="GitHub"></iframe>
20+
<nav class="@container flex items-center justify-end px-5 md:px-6 py-4 space-x-4">
21+
<div class="mr-3">
22+
<iframe src="https://ghbtns.com/github-btn.html?user=jargonsdev&repo=jargons.dev&type=watch&count=true&size=large" frameborder="0" scrolling="0" width="120" height="30" title="GitHub"></iframe>
23+
</div>
24+
<Profile
25+
isAuthed={isAuthed}
26+
userData={authedData}
27+
authUrl={getAuthUrl({path: "/"})}
28+
client:load
29+
/>
1730
</nav>
31+
1832
<main class="w-full flex flex-col grow max-w-screen-lg p-5 justify-center mx-auto">
1933
<!-- Title -->
2034
<div class="mb-4 md:mb-6">

src/pages/logout.astro

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
import doLogout from "../lib/actions/do-logout.js";
3+
4+
const { url: { searchParams }, redirect } = Astro;
5+
6+
const { isLoggedOut } = await doLogout(Astro);
7+
if (isLoggedOut) return redirect(searchParams.get("return_to") || "/");
8+
---

0 commit comments

Comments
 (0)