Skip to content
Open
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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"cSpell.words": ["Buil"]
"cSpell.words": ["Buil", "buildone"]
}
4 changes: 2 additions & 2 deletions chrome-extension/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ const manifest = {
},
version: packageJson.version,
description: '__MSG_extensionDescription__',
host_permissions: ['<all_urls>'],
permissions: ['storage', 'scripting', 'tabs', 'notifications'],
host_permissions: ['*://*.buildone.me/'],
permissions: ['storage', 'scripting', 'tabs', 'notifications', 'cookies'],
background: {
service_worker: 'background.js',
type: 'module',
Expand Down
2 changes: 1 addition & 1 deletion packages/hmr/lib/consts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const LOCAL_RELOAD_SOCKET_PORT = 8081;
export const LOCAL_RELOAD_SOCKET_PORT = 8083;
export const LOCAL_RELOAD_SOCKET_URL = `ws://localhost:${LOCAL_RELOAD_SOCKET_PORT}`;

export const DO_UPDATE = 'do_update';
Expand Down
25 changes: 25 additions & 0 deletions packages/storage/lib/impl/authStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { BaseStorage } from '../base/index.js';
import { createStorage, StorageEnum } from '../base/index.js';

type AccessToken = string | null;

type AccessTokenStorage = BaseStorage<AccessToken> & {
setAccessToken: (newAccessToken: string) => Promise<void>;
removeAccessToken: () => Promise<void>;
};

const storage = createStorage<AccessToken>('access-token-key', null, {
storageEnum: StorageEnum.Local,
liveUpdate: true,
});

// You can extend it with your own methods
export const authStorage: AccessTokenStorage = {
...storage,
setAccessToken: async (newAccessToken: string) => {
await storage.set(newAccessToken);
},
removeAccessToken: async () => {
await storage.set(null);
},
};
1 change: 1 addition & 0 deletions packages/storage/lib/impl/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './exampleThemeStorage.js';
export * from './authStorage.js';
2 changes: 1 addition & 1 deletion packages/ui/lib/global.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind utilities;
13 changes: 13 additions & 0 deletions pages/popup/public/arrow_dropdown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions pages/popup/public/arrow_dropdown_reverse.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
99 changes: 70 additions & 29 deletions pages/popup/src/Popup.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,79 @@
import { withErrorBoundary, withSuspense } from '@extension/shared';
import Input from '@src/components/Input';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import type { Dispatch, SetStateAction } from 'react';
import { createContext, useContext, useMemo, useState } from 'react';
import type {
FieldErrors,
UseFormHandleSubmit,
UseFormRegister,
UseFormSetValue,
UseFormTrigger,
UseFormWatch,
} from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import TodoCreate from './components/todo-create';

function Popup() {
const [link, setLink] = useState('');
export const todoFormSchema = z.object({
title: z.string().min(1, '제목을 입력해주세요').max(30, '30자 이내로 입력해주세요'),
link: z.union([z.literal(''), z.string().trim().url('올바른 URL을 입력해주세요')]),
});

export type TodoFormSchema = z.infer<typeof todoFormSchema>;

interface TodoModalFormContextProps {
register: UseFormRegister<TodoFormSchema>;
watch: UseFormWatch<TodoFormSchema>;
formState: {
errors: FieldErrors<TodoFormSchema>;
isValid: boolean;
};
trigger: UseFormTrigger<TodoFormSchema>;
handleSubmit: UseFormHandleSubmit<TodoFormSchema>;
setValue: UseFormSetValue<TodoFormSchema>;
selectedGoalId?: number;
setSelectedGoalId: Dispatch<SetStateAction<number | undefined>>;
}

const logo = 'popup/logo.svg';
const goGithubSite = () => chrome.tabs.create({ url: 'https://buildone.me' });
const TodoFormContext = createContext<TodoModalFormContextProps | null>(null);

useEffect(() => {
const getPageUrl = () => {
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
const { url } = tabs[0];
setLink(url!);
});
};
getPageUrl();
}, []);
export const useTodoFormContext = () => {
const context = useContext(TodoFormContext);
if (!context) {
throw new Error('할일 모달 폼 내부에서 context를 사용해주세요요');
}
return context;
};

function Popup() {
const { register, handleSubmit, watch, setValue, formState, trigger } = useForm<TodoFormSchema>({
resolver: zodResolver(todoFormSchema),
mode: 'onBlur',
defaultValues: {
title: '',
link: '',
},
});
const [selectedGoalId, setSelectedGoalId] = useState<number | undefined>();

const formContextValue = useMemo(
() => ({
handleSubmit,
setValue,
register,
watch,
formState,
trigger,
selectedGoalId,
setSelectedGoalId,
}),
[register, watch, formState, trigger, handleSubmit, setValue, selectedGoalId],
);

return (
<div className="w-full h-full px-16 py-24">
<header className="text-gray-900">
<button onClick={goGithubSite}>
<img src={chrome.runtime.getURL(logo)} className="App-logo" alt="logo" />
</button>
</header>
<main className="mt-16">
<form className="flex flex-col gap-16">
<Input id="link" value={link} readOnly />
<Input id="title" placeholder="할 일의 제목을 적어주세요." />
{/*목표 선택은 추후에 구현*/}
</form>
</main>
</div>
<TodoFormContext.Provider value={formContextValue}>
<TodoCreate />
</TodoFormContext.Provider>
);
}

Expand Down
50 changes: 39 additions & 11 deletions pages/popup/src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import type { Dispatch, PropsWithChildren, SetStateAction } from 'react';
import { createContext, useContext, useEffect, useState } from 'react';
import Login from './components/Login';
import PrivateRoute from './components/PrivateRoute';

import { useStorage } from '@extension/shared';
import { authStorage } from '@extension/storage';
import TanstackQueryProvider from '@src/lib/tanstack-query-provider';
import { reissueAccessToken } from './services/auth/token';
interface AuthContextProps {
authenticated: boolean;
setAuthenticated: Dispatch<SetStateAction<boolean>>;
Expand All @@ -25,25 +28,50 @@ export function useAuthContext() {

export const AuthProvider = ({ children }: PropsWithChildren) => {
const [authenticated, setAuthenticated] = useState(true);
const auth = useStorage(authStorage);

useEffect(() => {
const getNewToken = async () => {
const refreshToken = await chrome.cookies.get({ name: 'REFRESH_TOKEN', url: 'https://dev.api.buildone.me' });
if (!refreshToken) {
return await authStorage.removeAccessToken();
}
const newAccessToken = await reissueAccessToken();

if (newAccessToken) {
return await authStorage.setAccessToken(newAccessToken);
}
return await authStorage.removeAccessToken();
};
if (!auth) {
getNewToken();
}
}, [auth]);

useEffect(() => {
// 토큰이 있는지 검사
}, []);
if (auth) {
setAuthenticated(true);
} else {
setAuthenticated(false);
}
}, [auth]);

return <AuthContext.Provider value={{ authenticated, setAuthenticated }}>{children}</AuthContext.Provider>;
};

export default function Router() {
return (
<MemoryRouter>
<AuthProvider>
<Routes>
<Route path="/" element={<PrivateRoute />}>
<Route path="/" element={<Popup />} />
</Route>
<Route path="/login" element={<Login />} />
</Routes>
</AuthProvider>
<TanstackQueryProvider>
<AuthProvider>
<Routes>
<Route path="/" element={<PrivateRoute />}>
<Route path="/" element={<Popup />} />
</Route>
<Route path="/login" element={<Login />} />
</Routes>
</AuthProvider>
</TanstackQueryProvider>
</MemoryRouter>
);
}
8 changes: 5 additions & 3 deletions pages/popup/src/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
'use client';

import { forwardRef, InputHTMLAttributes, Ref, useState } from 'react';
import type { InputHTMLAttributes, Ref } from 'react';
import { forwardRef, useState } from 'react';

import Label from './Label';
import { cn } from '@extension/ui';

export const BASE_CLASS =
'flex items-center justify-center space-x-8 rounded-xl border border-slate-50 bg-slate-50 px-24 py-12 text-base font-normal focus-within:border-dark-blue-500 hover:border-dark-blue-300';
'flex items-center justify-center space-x-8 rounded-xl border border-slate-50 bg-slate-50 px-24 py-12 font-normal focus-within:border-dark-blue-500 hover:border-dark-blue-300';

export const RESPONSIVE_CLASS = 'h-44 w-343 md:h-48 md:w-612';

Expand All @@ -25,7 +27,7 @@ export default forwardRef(function Input(
return (
<>
{label && <Label htmlFor={id} label={label} />}
<div className={BASE_CLASS + ' ' + className}>
<div className={cn(BASE_CLASS, className)}>
<input
className="m-0 flex-1 appearance-none border-none bg-transparent p-0 text-slate-800 outline-none placeholder:text-slate-400"
type={type === 'password' ? (showPassword ? 'text' : 'password') : type}
Expand Down
8 changes: 3 additions & 5 deletions pages/popup/src/components/Label.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { LabelHTMLAttributes } from 'react';
import { cn } from '@extension/ui';
import type { LabelHTMLAttributes } from 'react';

interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
label: string;
}

export default function Label({ label, className, htmlFor, ...props }: LabelProps) {
return (
<label
className={'mb-12 block text-base font-semibold text-slate-800' + ' ' + className}
htmlFor={htmlFor}
{...props}>
<label className={cn('mb-12 block font-semibold text-slate-800', className)} htmlFor={htmlFor} {...props}>
{label}
</label>
);
Expand Down
7 changes: 6 additions & 1 deletion pages/popup/src/components/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { login } from '@src/services/auth';
import { ApiError } from '@src/lib/error';
import { useNavigate } from 'react-router';
import { LOGIN_ERROR_CODE } from '@src/constants/error';
import { authStorage } from '@extension/storage';

const loginSchema = z.object({
email: z.string().nonempty('이메일을 입력해주세요.').email('이메일 형식을 입력해주세요.'),
Expand Down Expand Up @@ -50,10 +51,14 @@ export default function Login() {

const onSubmit = async (data: LoginSchema) => {
try {
await login(data.email, data.password);
const { data: res } = await login(data.email, data.password);

const { accessToken } = res.credentials;
authStorage.setAccessToken(accessToken);

navigate('/');
} catch (error: unknown) {
console.log(error);
if (error instanceof ApiError) {
if (
error.code === LOGIN_ERROR_CODE.INVALID_EMAIL_FORMAT ||
Expand Down
4 changes: 3 additions & 1 deletion pages/popup/src/components/PrivateRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useStorage } from '@extension/shared';
import { authStorage } from '@extension/storage';
import { Navigate, Outlet } from 'react-router';

const PrivateRoute = () => {
const auth = null; // determine if authorized, from context or however you're doing it
const auth = useStorage(authStorage);

// If authorized, return an outlet that will render child elements
// If not, return element that will navigate to login page
Expand Down
4 changes: 2 additions & 2 deletions pages/popup/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const ButtonVariants = cva('flex items-center justify-center px-18 py-8 font-sem
round: 'rounded-3xl',
},
size: {
lg: 'min-w-291 min-h-48 text-base',
md: 'min-w-150 min-h-44 text-base',
lg: 'min-w-291 min-h-48',
md: 'min-w-150 min-h-44',
sm: 'min-w-84 min-h-36 text-sm',
},
},
Expand Down
Loading
Loading