Skip to content

Commit

Permalink
Logout button will now actually logout the user (#12)
Browse files Browse the repository at this point in the history
* bug: Upgraded Next to fix issue with comments in production output

See vercel/next.js#36998 for details

* bug: Improved placement of logout menu

Also added listener to close the menu when a click is registered outside of the component

* refactor: Moved login menu to individual files for better seperation

* feat: Implemented logout functionality
  • Loading branch information
OliverFlecke committed May 27, 2022
1 parent 235de3d commit a9bed30
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 163 deletions.
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -18,7 +18,7 @@
"deploy": "yarn build && gh-pages -t true -d build"
},
"dependencies": {
"@oliverflecke/components-react": "^0.3.3",
"@oliverflecke/components-react": "^0.5.1",
"@visx/curve": "^2.1.0",
"@visx/group": "^2.1.0",
"@visx/legend": "^2.2.2",
Expand Down Expand Up @@ -76,7 +76,7 @@
"gh-pages": "^3.2.3",
"husky": "^7.0.4",
"jest": "^28.0.3",
"next": "^12.1.5",
"next": "^12.1.6",
"postcss": "^8.4.5",
"postcss-cli": "^9.1.0",
"postcss-import": "^14.0.1",
Expand Down
16 changes: 10 additions & 6 deletions src/features/Header.tsx
Expand Up @@ -7,14 +7,14 @@ import LoginState from './login/LoginState';
import Navigation from './Navigation';
import { getMyUser } from './user/userApi';

const returnUrl =
process.env.NODE_ENV === 'development'
? 'https://localhost:3000'
: 'https://finance.oliverflecke.me';

const Header: React.FC = () => {
const [user, setUser] = useState<User | null>(null);

const returnUrl =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000'
: 'https://finance.oliverflecke.me';

useEffect(() => {
getMyUser().then(user => {
if (user) setUser(user);
Expand All @@ -26,7 +26,11 @@ const Header: React.FC = () => {
<Navigation />
<div>
<div className="flex flex-row items-center justify-center space-x-4">
<LoginState user={user} authorizeUrl={`${baseUri}/signin?returnUrl=${returnUrl}`} />
<LoginState
user={user}
logoutUrl={`${baseUri}/signout?returnUrl=${returnUrl}`}
authorizeUrl={`${baseUri}/signin?returnUrl=${returnUrl}`}
/>
<ClientOnly>
<SettingsMenu />
</ClientOnly>
Expand Down
13 changes: 13 additions & 0 deletions src/features/login/LoginButton.tsx
@@ -0,0 +1,13 @@
import React from 'react';

interface LoginButtonProps {
authorizeUrl: string;
}

const LoginButton: React.FC<LoginButtonProps> = ({ authorizeUrl }) => (
<a className="btn btn-primary" href={authorizeUrl}>
Login
</a>
);

export default LoginButton;
25 changes: 25 additions & 0 deletions src/features/login/LoginMenuProps.tsx
@@ -0,0 +1,25 @@
import React from 'react';
import { IoLogOutOutline } from 'react-icons/io5';

interface LoginMenuProps {
isOpen: boolean;
logoutUrl: string;
}

const LoginMenu: React.FC<LoginMenuProps> = ({ isOpen, logoutUrl }) => (
<div
className={`${
isOpen ? '' : 'hidden'
} absolute top-full right-0 z-10 rounded bg-gray-100 py-4 shadow outline group-hover:block dark:bg-gray-700`}
>
<a
href={logoutUrl}
className="btn flex items-center space-x-2 hover:text-gray-900 hover:underline dark:hover:text-gray-400"
>
<IoLogOutOutline className="inline" />
<span className="align-middle">Logout</span>
</a>
</div>
);

export default LoginMenu;
89 changes: 32 additions & 57 deletions src/features/login/LoginState.tsx
@@ -1,75 +1,50 @@
import React, { useState } from 'react';
import { IoLogOutOutline } from 'react-icons/io5';
import { useOnOutsideMouseDown } from '@oliverflecke/components-react';
import React, { useCallback, useRef, useState } from 'react';
import { User } from 'utils/githubAuth';
import LoginButton from './LoginButton';
import LoginMenu from './LoginMenuProps';
import UserAvatar from './UserAvatarProps';

interface LoginStateProps {
user: User | null;
authorizeUrl: string;
logout?: () => void;
logoutUrl: string;
}

const LoginState: React.FC<LoginStateProps> = ({ user, authorizeUrl, logout }: LoginStateProps) => {
const LoginState: React.FC<LoginStateProps> = ({ user, authorizeUrl, logoutUrl }) =>
user === null ? (
<LoginButton authorizeUrl={authorizeUrl} />
) : (
<LoginDropDownMenu user={user} logoutUrl={logoutUrl} />
);

export default LoginState;

interface LoginDropDownMenuProps {
user: User;
logoutUrl: string;
}

const LoginDropDownMenu: React.FC<LoginDropDownMenuProps> = ({ user, logoutUrl }) => {
const [isOpen, setIsOpen] = useState(false);

if (user === null) {
return <LoginButton authorizeUrl={authorizeUrl} />;
}
const ref = useRef<HTMLDivElement>(null);
useOnOutsideMouseDown(
ref,
useCallback(() => setIsOpen(false), [])
);

console.debug(logoutUrl);

return (
<div className="flex items-center space-x-4">
<div ref={ref} className="relative flex items-center space-x-4">
<span className="hidden sm:inline">{user.login}</span>
<div className="group" onMouseLeave={() => setIsOpen(false)}>
<button onClick={() => setIsOpen((x) => !x)}>
<div className="group">
<button onClick={() => setIsOpen(x => !x)}>
<UserAvatar user={user} />
</button>
</div>
<Menu isOpen={isOpen} logout={logout} />
<LoginMenu isOpen={isOpen} logoutUrl={logoutUrl} />
</div>
);
};

export default LoginState;

interface LoginButtonProps {
authorizeUrl: string;
}

const LoginButton = ({ authorizeUrl }: LoginButtonProps) => (
<a className="btn btn-primary" href={authorizeUrl}>
Login
</a>
);

interface UserAvatarProps {
user: User;
}

const UserAvatar = ({ user }: UserAvatarProps) => (
<img
src={user.avatar_url}
alt="Avatar of the logged in user"
className="max-h-10 rounded-full"
loading="lazy"
/>
);

interface MenuProps {
isOpen: boolean;
logout?: () => void;
}

const Menu = ({ isOpen, logout }: MenuProps) => (
<div
className={`${
isOpen ? '' : 'hidden'
} group-hover:block absolute right-0 rounded py-4 shadow bg-gray-100 dark:bg-gray-700`}
>
<button
onClick={logout}
className="btn flex items-center space-x-2 hover:text-gray-900 dark:hover:text-gray-400 hover:underline"
>
<IoLogOutOutline className="inline" />
<span className="align-middle">Logout</span>
</button>
</div>
);
17 changes: 17 additions & 0 deletions src/features/login/UserAvatarProps.tsx
@@ -0,0 +1,17 @@
import React from 'react';
import { User } from 'utils/githubAuth';

interface UserAvatarProps {
user: User;
}

const UserAvatar: React.FC<UserAvatarProps> = ({ user }) => (
<img
src={`${user.avatar_url}&s=80`}
alt="Avatar of the logged in user"
className="max-h-10 rounded-full"
loading="lazy"
/>
);

export default UserAvatar;
4 changes: 2 additions & 2 deletions src/features/user/userApi.ts
Expand Up @@ -5,6 +5,6 @@ export function getMyUser(): Promise<User> {
return fetch(`${baseUri}/${apiVersion}/user/me`, {
credentials: 'include',
})
.then((res) => res.json())
.catch((err) => console.log(err));
.then(res => res.json())
.catch(err => console.log(err));
}
9 changes: 5 additions & 4 deletions src/utils/githubAuth.ts
@@ -1,5 +1,6 @@
import dotenv from 'dotenv';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { baseUri } from '../features/apiBase';
dotenv.config();

const authorizeUrl = 'https://github.com/login/oauth/authorize';
Expand Down Expand Up @@ -49,7 +50,7 @@ export function useGithubUser(): UseGithubUserHook {
}

export function getUser(state: string): Promise<User | null> {
return new Promise<User | null>((resolve) => {
return new Promise<User | null>(resolve => {
const user = getUserFromLocalStorage();
if (user) {
resolve(user);
Expand Down Expand Up @@ -77,7 +78,7 @@ export function getUser(state: string): Promise<User | null> {
Accept: 'application/json',
},
})
.then(async (res) => {
.then(async res => {
if (res.status === 200) {
const body: AuthorizeResponse = await res.json();
const user = await getUserFromGithub(body.access_token);
Expand All @@ -91,7 +92,7 @@ export function getUser(state: string): Promise<User | null> {
// Remove query parameters from navigation bar
window.history.replaceState({}, document.title, window.location.pathname);
})
.catch((err) => console.debug(err));
.catch(err => console.debug(err));
});
}

Expand All @@ -100,7 +101,7 @@ function getUserFromGithub(token: string): Promise<User> {
headers: {
Authorization: `token ${token}`,
},
}).then(async (res) => (await res.json()) as User);
}).then(async res => (await res.json()) as User);
}

function getUserFromLocalStorage(): User | null {
Expand Down

0 comments on commit a9bed30

Please sign in to comment.