Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add basic UI #2043

Merged
merged 3 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,16 @@ BASE_STORAGE_PATH =
PAYMENT_TOKEN =
PAYMENT_ID =
PAYMENT_SERVICE =

# OAuth 2.0 for apple login and google login
GOOGLE_CLIENT_ID = google-client-id
GOOGLE_CLIENT_SECRET = google-client-secret

APPLE_CLIENT_ID = com.company.app
APPLE_CLIENT_SECRET = apple-client-secret
APPLE_KEY_ID = apple-key-id
APPLE_TEAM_ID = apple-team-id
APPLE_PRIVATE_KEY = apple-private-key

NEXTAUTH_URL = https://isunfa.com/
NEXTAUTH_SECRET = generated-random-secret
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "iSunFA",
"version": "0.8.0+4",
"version": "0.8.0+5",
"private": false,
"scripts": {
"dev": "next dev",
Expand Down Expand Up @@ -35,8 +35,10 @@
"formidable": "^3.5.1",
"i18next": "^23.11.5",
"jest-mock-extended": "^3.0.7",
"jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21",
"next": "^14.2.5",
"next-auth": "^4.24.7",
"next-i18next": "^15.2.0",
"next-session": "^4.0.5",
"nodemailer": "^6.9.8",
Expand All @@ -60,6 +62,7 @@
"@testing-library/react": "^14.3.1",
"@types/cookie": "^0.6.0",
"@types/jest": "^29.5.11",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20",
"@types/nodemailer": "^6.4.15",
"@types/react": "^18",
Expand Down
4 changes: 4 additions & 0 deletions public/icons/apple_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions public/icons/google_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions public/images/login_bg.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/add_journal_body/add_journal_body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const AddJournalBody = () => {
content: t('JOURNAL.LEAVE_HINT_CONTENT'), // 'Are you sure you want to leave the form?',
submitBtnStr: t('JOURNAL.LEAVE'),
submitBtnFunction: () => backClickHandler(),
backBtnStr: t('JOURNAL.CANCEL'),
backBtnStr: t('COMMON.CANCEL'),
messageType: MessageType.WARNING,
};

Expand Down
6 changes: 3 additions & 3 deletions src/components/kyc/kyc_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const KYCForm = () => {
messageType: MessageType.SUCCESS,
title: t('KYC.SUBMIT_SUCCESS'),
content: t('KYC.SUBMIT_SUCCESS_MESSAGE'),
backBtnStr: t('KYC.CANCEL'),
backBtnStr: t('COMMON.CANCEL'),
submitBtnStr: t('KYC.CONFIRM'),
submitBtnFunction: () => {
messageModalVisibilityHandler();
Expand All @@ -115,7 +115,7 @@ const KYCForm = () => {
title: t('KYC.SUBMIT_FAILED'),
content: t('KYC.CONTACT_SERVICE_TEAM'),
subMsg: t('KYC.SUBMIT_FAILED_MESSAGE', code),
backBtnStr: t('KYC.CANCEL'),
backBtnStr: t('COMMON.CANCEL'),
submitBtnStr: t('KYC.CONFIRM'),
submitBtnFunction: () => {
messageModalVisibilityHandler();
Expand All @@ -130,7 +130,7 @@ const KYCForm = () => {
subMsg: t('KYC.INCOMPLETE_FORM_SUB_MESSAGE', { fields: missingFields.join(', ') }),
content: t('KYC.CONTACT_SERVICE_TEAM'),
submitBtnStr: t('KYC.CONFIRM'),
backBtnStr: t('KYC.CANCEL'),
backBtnStr: t('COMMON.CANCEL'),
submitBtnFunction: () => {
messageModalVisibilityHandler();
goCompanyInfo();
Expand Down
2 changes: 1 addition & 1 deletion src/components/kyc/kyc_leave_button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const LeaveButton = () => {
subMsg: `${t('KYC.ARE_YOU_SURE_YOU_WANT_TO_LEAVE_THIS_PAGE')} ?`,
submitBtnStr: t('KYC.LEAVE_NOW'),
submitBtnFunction: handleBack,
backBtnStr: t('KYC.CANCEL'),
backBtnStr: t('COMMON.CANCEL'),
});
messageModalVisibilityHandler();
};
Expand Down
50 changes: 50 additions & 0 deletions src/components/login_confirm_modal/login_confirm_modal.beta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { useTranslation } from 'react-i18next';

interface ILoginConfirmProps {
isModalVisible: boolean;
modalData: {
title: string;
content: string;
buttonText: string;
};
onAgree: () => void;
onCancel: () => void;
}

const LoginConfirmModal: React.FC<ILoginConfirmProps> = ({
isModalVisible,
modalData,
onAgree,
onCancel,
}) => {
const { t } = useTranslation('common');
return (
isModalVisible && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-full max-w-lg rounded-lg bg-white p-6 shadow-lg">
<h2 className="mb-4 text-2xl font-bold">{modalData.title}</h2>
<p className="mb-6">{modalData.content}</p>
<div className="flex justify-end space-x-4">
<button
type="button"
onClick={onCancel}
className="rounded-lg bg-gray-200 px-4 py-2 text-gray-700"
>
{t('COMMON.CANCEL')}
</button>
<button
type="button"
onClick={onAgree}
className="rounded-lg bg-blue-600 px-4 py-2 text-white"
>
{modalData.buttonText}
</button>
</div>
</div>
</div>
)
);
};

export default LoginConfirmModal;
203 changes: 203 additions & 0 deletions src/components/login_page_body/login_page_body.beta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import React from 'react';
import { signIn, SignInResponse } from 'next-auth/react';
import Image from 'next/image';
import { useGlobalCtx } from '@/contexts/global_context';
import APIHandler from '@/lib/utils/api_handler';
import { IUser } from '@/interfaces/user';
import { APIName } from '@/constants/api_connection';
import { useRouter } from 'next/router';
import { ISUNFA_ROUTE } from '@/constants/url';

enum AuthWith {
GOOGLE = 'google',
APPLE = 'apple',
}

const LoginPageBody = () => {
const router = useRouter();
const {
isAgreeWithInfomationConfirmModalVisible,
agreeWithInfomationConfirmModalVisibilityHandler,
TOSNPrivacyPolicyConfirmModalCallbackHandler,
} = useGlobalCtx();
const { trigger: logInTrigger } = APIHandler<{
user: IUser;
hasReadAgreement: boolean;
}>(APIName.LOGIN);
const { trigger: agreementTrigger } = APIHandler<null>(APIName.AGREEMENT);

const handleUserAgree = async (authWith: AuthWith) => {
try {
// 呼叫 API 紀錄用戶已同意條款
const response = await agreementTrigger({
body: { authWith, agreement: true },
});

const { success, code } = response;
// eslint-disable-next-line no-console
console.log('紀錄用戶同意條款:', success, code);
router.push(ISUNFA_ROUTE.SELECT_COMPANY);
} catch (error) {
// eslint-disable-next-line no-console
console.error('紀錄用戶同意條款時發生錯誤:', error);
}
};

// 新增:呼叫後端 signInAPI
const callLogInAPI = async (authWith: AuthWith, oauthResponse: SignInResponse | undefined) => {
try {
const response = await logInTrigger({
body: { authWith, oauthResponse },
});

// 處理後端返回的 user 和 hasReadAgreement 資料
const { success, data } = response;
if (success && data) {
const { user, hasReadAgreement } = data;
if (!hasReadAgreement && !isAgreeWithInfomationConfirmModalVisible) {
// 如果用戶尚未同意條款,顯示同意條款的模態框
TOSNPrivacyPolicyConfirmModalCallbackHandler(() => handleUserAgree(authWith));
agreeWithInfomationConfirmModalVisibilityHandler();
} else {
// 用戶已經同意條款,直接進行登入後的邏輯處理。TODO: (20240812-Tzuhan) route to select-company page
// eslint-disable-next-line no-console
console.log('用戶已登入:', user);
router.push(ISUNFA_ROUTE.SELECT_COMPANY);
}
} else {
// eslint-disable-next-line no-console
console.error('登入 API 錯誤:', response);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('登入 API 錯誤:', error);
}
};

// 登入處理函數,呼叫 OAuth 登入並獲取響應後再呼叫 signInAPI
const loginHandler = async (authWith: AuthWith) => {
try {
// eslint-disable-next-line no-console
console.log('loginHandler:', authWith);
const oauthResponse = await signIn(authWith, { redirect: false });

if (oauthResponse?.error) {
// eslint-disable-next-line no-console
console.error('OAuth 登入失敗:', oauthResponse.error);
} else {
// 呼叫後端 signInAPI 處理登入
await callLogInAPI(authWith, oauthResponse);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('登入處理函數錯誤:', error);
}
};

// 處理 Apple 登入
const AuthWithApple = () => loginHandler(AuthWith.APPLE);

// 處理 Google 登入
const AuthWithGoogle = () => loginHandler(AuthWith.GOOGLE);

return (
<div className="relative flex h-screen flex-col items-center justify-center text-center">
{/* 背景圖片 */}
<div className="absolute inset-0 z-0">
<Image
src="/images/login_bg.svg"
alt="Background"
layout="fill"
objectFit="cover"
quality={100}
/>
{/* 白色透明遮罩 */}
<div className="absolute inset-0 bg-white opacity-10"></div>
</div>
<div className="z-1 mb-8 flex flex-col items-center">
<h1 className="mb-6 text-4xl font-bold text-gray-800">Log In</h1>
<div className="mx-2.5 flex flex-col justify-center rounded-full">
<div className="flex aspect-square items-center justify-center px-5 lg:px-10">
<div className="mx-2 hidden items-center justify-center lg:flex">
{/* 匿名頭像 */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="201"
height="200"
fill="none"
viewBox="0 0 201 200"
>
<path
fill="#CDD1D9"
d="M.5 100C.5 44.772 45.272 0 100.5 0s100 44.772 100 100-44.772 100-100 100S.5 155.228.5 100z"
></path>
<g clipPath="url(#clip0_13_13411)">
<path
fill="#7F8A9D"
fillRule="evenodd"
d="M100.5 68.013c-11.942 0-21.623 9.68-21.623 21.622 0 8.151 4.51 15.249 11.17 18.934a31.953 31.953 0 00-19.976 20.439 2.286 2.286 0 002.177 2.984h56.503a2.284 2.284 0 002.176-2.984 31.956 31.956 0 00-19.975-20.439c6.661-3.685 11.171-10.782 11.171-18.934 0-11.942-9.681-21.622-21.623-21.622z"
clipRule="evenodd"
></path>
</g>
<defs>
<clipPath id="clip0_13_13411">
<path fill="#fff" d="M0 0H64V64H0z" transform="translate(68.5 68)"></path>
</clipPath>
</defs>
</svg>
</div>

<div className="mx-2 flex items-center justify-center lg:hidden">
{/* 匿名頭像 */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="100"
height="100"
fill="none"
viewBox="0 0 201 200"
>
<path
fill="#CDD1D9"
d="M.5 100C.5 44.772 45.272 0 100.5 0s100 44.772 100 100-44.772 100-100 100S.5 155.228.5 100z"
></path>
<g clipPath="url(#clip0_13_13411)">
<path
fill="#7F8A9D"
fillRule="evenodd"
d="M100.5 68.013c-11.942 0-21.623 9.68-21.623 21.622 0 8.151 4.51 15.249 11.17 18.934a31.953 31.953 0 00-19.976 20.439 2.286 2.286 0 002.177 2.984h56.503a2.284 2.284 0 002.176-2.984 31.956 31.956 0 00-19.975-20.439c6.661-3.685 11.171-10.782 11.171-18.934 0-11.942-9.681-21.622-21.623-21.622z"
clipRule="evenodd"
></path>
</g>
<defs>
<clipPath id="clip0_13_13411">
<path fill="#fff" d="M0 0H64V64H0z" transform="translate(68.5 68)"></path>
</clipPath>
</defs>
</svg>
</div>
</div>
</div>
<div className="flex flex-col space-y-4">
<button
type="button"
onClick={AuthWithGoogle}
className="flex w-72 items-center justify-center space-x-2 rounded-lg border border-gray-300 bg-white py-3 shadow-md"
>
<Image src="/icons/google_logo.svg" alt="Google" className="h-6 w-6" />
<span className="font-semibold">Log In with Google</span>
</button>
<button
type="button"
onClick={AuthWithApple}
className="flex w-72 items-center justify-center space-x-2 rounded-lg bg-black py-3 text-white shadow-md"
>
<Image src="/icons/apple_logo.svg" alt="Apple" className="h-6 w-6" />
<span className="font-semibold">Log In with Apple</span>
</button>
</div>
</div>
</div>
);
};

export default LoginPageBody;
Loading