Skip to content

Commit

Permalink
Merge pull request #8 from khoaxuantu/dev
Browse files Browse the repository at this point in the history
Add guestbook
  • Loading branch information
khoaxuantu authored May 13, 2023
2 parents f0b6f93 + b848c26 commit 9d27c48
Show file tree
Hide file tree
Showing 46 changed files with 916 additions and 281 deletions.
6 changes: 6 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# For fetching backend API
REACT_APP_BACKEND_DOMAIN=

# For oauth
REACT_APP_GOOGLE_CLIENT_ID=
REACT_APP_GITHUB_CLIENT_ID=
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"homepage": "https://xuankhoatu.com/",
"dependencies": {
"@react-oauth/google": "^0.11.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
Expand Down
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link href="https://esm.sh/highlight.js@11/styles/github.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/logo/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/logo/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/logo/favicon-16x16.png">
Expand Down
2 changes: 1 addition & 1 deletion public/logo/site.webmanifest
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{"name":"","short_name":"","icons":[{"src":"/logo/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/logo/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
29 changes: 20 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import React from 'react';
import { lazy } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import * as Site from './components/site';
import MainLayout from './components/layouts/main';
import BlogLayout from './components/layouts/blog';
import MenuBuilder from './components/menu/page';
import AboutPage from './components/about/page';
import ProjectsPage from './components/projects/page';
import { AnimatePresence } from 'framer-motion';
import ScrollToTop from './components/scrollToTop';

const Oauth = lazy(() => import('./components/oauth'));
const BlogsPage = lazy(() => import('./components/blogs/listpage'));
const SingleBlogPage = lazy(() => import('./components/blogs/singlepage'));
const GuestbookPage = lazy(() => import(`./components/guestbook/page`));


function App() {
const location = useLocation();
Expand All @@ -12,14 +21,16 @@ function App() {
<AnimatePresence mode='wait'>
<ScrollToTop />
<Routes location={location} key={location.pathname}>
<Route path='/' element={<Site.Layout />}>
<Route index element={<Site.MenuBuilder />} />
<Route path='about' element={<Site.AboutPage />} />
<Route path='projects' element={<Site.ProjectsPage />} />
<Route path='blogs' element={<Site.BlogsLayout />}>
<Route index element={<Site.BlogsPage />} />
<Route path=':blogId' element={<Site.SingleBlogPage />} />
<Route path='/' element={<MainLayout />}>
<Route index element={<MenuBuilder />} />
<Route path='about' element={<AboutPage />} />
<Route path='projects' element={<ProjectsPage />} />
<Route path='blogs' element={<BlogLayout />}>
<Route index element={<BlogsPage />} />
<Route path=':blogId' element={<SingleBlogPage />} />
</Route>
<Route path='guestbook' element={<GuestbookPage />} />
<Route path='oauth' element={<Oauth />} />
</Route>
</Routes>
</AnimatePresence>
Expand Down
File renamed without changes.
23 changes: 23 additions & 0 deletions src/components/about/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Link } from "react-router-dom";

export default function AboutHeaderGrp () {
return (
<div className='header-grp'>
<div className="header-txt">
<b>Hi there! I'm Tu</b>
</div>
<div className='description-txt mt-2'>
<b>Xuan Khoa Tu Nguyen | 阮春科秀 | Tu | Nguyễn Xuân Khoa Tú</b>
</div>
<div className="description-txt mt-3">
~ You can call me by any name ~
</div>
<div className='description-txt mt-3'>
<Link to="/">Menu</Link> | {" "}
<a href="https://drive.google.com/file/d/1UdFJgT35HysZGTpfk86I3E8Mcg2vrfv3/view?usp=share_link" target='_blank' rel="noreferrer">Resume</a> | {" "}
<a href="https://drive.google.com/file/d/1XxdNzIyDktPseomnn0HszhgMyfEW8TtV/view?usp=share_link" target='_blank' rel='noreferrer'>CV</a>
</div>
<hr></hr>
</div>
);
}
File renamed without changes.
25 changes: 25 additions & 0 deletions src/components/about/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { motion as m } from "framer-motion";
import AboutHeaderGrp from "./header";
import AboutIntro from "./introduction";
import Works from "./works";
import Education from "./edu";
import { AboutProject } from "../projects/projects";
import Copyright from "../copyright";

export default function AboutPage() {
return (
<m.div
transition={{ duration: 0.8 }}
exit={{ opacity: 0 }}
className="container page-wrapper transition-page">
<AboutHeaderGrp />
<div className="content-grp">
<AboutIntro />
<Education />
<Works />
<AboutProject />
</div>
<Copyright copyright_class="page-copyright pb-3" />
</m.div>
);
}
File renamed without changes.
File renamed without changes.
27 changes: 27 additions & 0 deletions src/components/blogs/contents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { blogInfoList } from "../../lib/general_info";
import { BlogsList, SingleBlog } from "./blogs";

export function BlogsContent() {
/* blogInfoList needs sorting first */
blogInfoList.sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
if (dateA < dateB) return 1;
else if (dateA > dateB) return -1;
else return 0;
});

return (
<div className="blog-wrapper">
<BlogsList blogInfoList={blogInfoList} />
</div>
);
}

export function SingleBlogContent(props: {id: string}) {
return (
<article className="single-blog-wrapper transition-blog">
<SingleBlog id={props.id} />
</article>
);
}
25 changes: 25 additions & 0 deletions src/components/blogs/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { blogInfoDict } from "../../lib/general_info";

export function BlogsPageHeaderGrp() {
return (
<div className='header-grp'>
<div className='header-txt'>
<b>Blogs</b>
</div>
<div className='description-txt mt-2'>
Some notes, some ideas, some opinions
</div>
<hr />
</div>
);
}

export function SingleBlogPageHeaderGrp(props: {id: string}) {
return (
<div className='header-grp'>
<div className='header-txt'>
<b>{blogInfoDict[props.id].title}</b>
</div>
</div>
);
}
11 changes: 11 additions & 0 deletions src/components/blogs/listpage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BlogsPageHeaderGrp } from "./header";
import { BlogsContent } from "./contents";

export default function BlogsPage() {
return (
<>
<BlogsPageHeaderGrp />
<BlogsContent />
</>
);
}
15 changes: 15 additions & 0 deletions src/components/blogs/singlepage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useParams } from "react-router-dom";
import { SingleBlogPageHeaderGrp } from "./header";
import { SingleBlogContent } from "./contents";

export default function SingleBlogPage() {
const { blogId } = useParams();

return (
<>
<SingleBlogPageHeaderGrp id={blogId as string} />
<hr></hr>
<SingleBlogContent id={blogId as string} />
</>
);
}
50 changes: 0 additions & 50 deletions src/components/content_grp.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/copyright.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ function Copyright(props: { copyright_class?: string }) {
return (
<div className={`${props.copyright_class} pt-4`}>
<div className="proj-github-ref">
<a href="https://github.com/khoaxuantu/tuslipid" target="_blank" rel="noreferrer">
<a href="https://github.com/khoaxuantu/tuslipid" target="_blank" rel="noreferrer" aria-label="web source code">
<BsGithub opacity={0.8} color="black" />
</a>
</div>
Expand Down
87 changes: 87 additions & 0 deletions src/components/guestbook/actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { LoginButton } from "../../lib/factory/buttonBase";
import { useNavigate } from "react-router-dom";
import { oauthInfoList } from "../../lib/general_info";
import { useGoogleLogin } from "@react-oauth/google";

const {
REACT_APP_BACKEND_DOMAIN,
REACT_APP_GITHUB_CLIENT_ID } = process.env;

export function SignIn() {

/**
* Login with Github will redirect user to an authorization callback URL
* so I redirect to a new route "/oauth" instead of back to this route "/guestbook"
*
* At route "/oauth", the received authorization code will be fetched to
* the backend api to process authentication. After backend responses
* a access_token successfully, users will be redirected back to "/guestbook"
*/
function loginWithGithub() {
window.location.assign("https://github.com/login/oauth/authorize?client_id=" + REACT_APP_GITHUB_CLIENT_ID);
}

/**
* Login with Google uses @react-oauth/google library, which authorizes
* in popup mode. The code is returned to in-browser's app callback handler,
* without users needing to leave the website
*
* https://developers.google.com/identity/oauth2/web/guides/how-user-authz-works#when_using_the_auth_code_flow
*/
let navigate = useNavigate();
const loginWithGoogle = useGoogleLogin({
onSuccess: async ({code}) => {
if (localStorage.getItem("accessToken") === null) {
await fetch(`${REACT_APP_BACKEND_DOMAIN}/oauth/google/access_token?code=` + code, {
method: "GET"
})
.then(response => response.json())
.then(data => {
if (data.access_token) {
localStorage.setItem("accessToken", data.access_token);
localStorage.setItem("third-party", "google");
return navigate("/guestbook");
}
});
}
},
flow: "auth-code",
});

const onClickHandlers: any[] = [
loginWithGithub,
() => loginWithGoogle()
];

return (
<>
{
oauthInfoList.map((oauth, index) => {
return (
<div key={oauth.name} className="col-6 btn-oauth-box">
<LoginButton
onClick={onClickHandlers[index]}
{...oauth} />
</div>
);
})
}
</>
);
}

export function SignOut() {
let navigate = useNavigate();

function logOut() {
localStorage.removeItem("third-party");
localStorage.removeItem("data");
return navigate("/guestbook");
}

return (
<button className="btn btn-rect" onClick={logOut}>
Sign out
</button>
);
}
Loading

0 comments on commit 9d27c48

Please sign in to comment.