Skip to content
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
VITE_BASE_URL=
VITE_API_BASE_URL=
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ _Link to the backend repo:_ https://github.com/chertik77/TaskPro-backend

## Languages and Tools

![Languages and Tools](https://skills.syvixor.com/api/icons?i=ts,react,dndkit,baseui,tanstack,stanjs,axios,datefns,reactdatepicker,reacthookform,valibot,tailwind,tailwindmerge,fusejs,commitlint,eslint,prettier,githubactions,fsd,yarn,vercel,vite,vscode,figma&perline=8)
![Languages and Tools](https://skills.syvixor.com/api/icons?i=ts,react,dndkit,baseui,tanstack,betterauth,stanjs,axios,datefns,reactdatepicker,reacthookform,valibot,tailwind,tailwindmerge,fusejs,commitlint,eslint,prettier,githubactions,fsd,yarn,vercel,vite,vscode,figma&perline=9)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"axios": "^1.18.1",
"axios-auth-refresh": "^5.0.2",
"better-auth": "^1.6.23",
"chrono-node": "^2.9.1",
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
Expand Down
39 changes: 12 additions & 27 deletions src/app/providers/RouteProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,18 @@
import { useQueryClient } from '@tanstack/react-query'
import { RouterProvider as TanStackRouterProvider } from '@tanstack/react-router'

import { sessionService } from '@/entities/session'
import { userQueries } from '@/entities/user'

import { attachInternalApiMemoryStorage } from '@/shared/api'
import { router } from '@/shared/lib'
import { GlobalError, Loader } from '@/shared/ui'

export const RouterProvider = () => {
const queryClient = useQueryClient()

attachInternalApiMemoryStorage({
refreshTokens: sessionService.refreshTokens,
logout: () => queryClient.resetQueries({ queryKey: userQueries.current() })
})

return (
<TanStackRouterProvider
router={router}
defaultPendingComponent={() => (
<div className='fixed top-0 right-0 block h-12 w-screen'>
<div
className='bg-soft-green flex h-screen items-center justify-center'>
<Loader className='size-12' />
</div>
export const RouterProvider = () => (
<TanStackRouterProvider
router={router}
defaultPendingComponent={() => (
<div className='fixed top-0 right-0 block h-12 w-screen'>
<div className='bg-soft-green flex h-screen items-center justify-center'>
<Loader className='size-12' />
</div>
)}
defaultErrorComponent={() => <GlobalError />}
/>
)
}
</div>
)}
defaultErrorComponent={() => <GlobalError />}
/>
)
12 changes: 0 additions & 12 deletions src/entities/session/api/endpoints.ts

This file was deleted.

34 changes: 16 additions & 18 deletions src/entities/session/api/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,48 @@ import type { SigninDto, SignupDto } from './types'

import { parse } from 'valibot'

import { axiosInstance } from '@/shared/api'
import { authClient } from '@/shared/api'
import { env } from '@/shared/config'

import {
SessionResponseDtoSchema,
SigninDtoSchema,
SignupDtoSchema
} from './contracts'
import { sessionApiEndpoints } from './endpoints'

export const sessionService = {
async signup(data: SignupDto) {
const signupDto = parse(SignupDtoSchema, data)

console.log(signupDto)
const response = await authClient.signUp.email({
...signupDto,
theme: 'light'
})

const response = await axiosInstance.post(
sessionApiEndpoints.signup,
signupDto
)

const parsedData = parse(SessionResponseDtoSchema, response.data)
const parsedData = parse(SessionResponseDtoSchema, response)

return parsedData
},

async signin(data: SigninDto) {
const signinDto = parse(SigninDtoSchema, data)

const response = await axiosInstance.post(
sessionApiEndpoints.signin,
signinDto,
{ skipAuthRefresh: true }
)
const response = await authClient.signIn.email(signinDto)

const parsedData = parse(SessionResponseDtoSchema, response.data)
const parsedData = parse(SessionResponseDtoSchema, response)

return parsedData
},

async refreshTokens() {
await axiosInstance.post(sessionApiEndpoints.refresh)
async continueWithSocial(provider: 'google' | 'microsoft') {
await authClient.signIn.social({
provider,
callbackURL: env.VITE_BASE_URL,
errorCallbackURL: env.VITE_BASE_URL + '?error=oauth_error'
})
},

async logout() {
await axiosInstance.post(sessionApiEndpoints.logout)
await authClient.signOut()
}
}
1 change: 0 additions & 1 deletion src/entities/session/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * as SessionDtoTypes from './api/types'
export * as SessionDtoContracts from './api/contracts'
export { sessionService } from './api/service'
export { sessionApiEndpoints } from './api/endpoints'
22 changes: 19 additions & 3 deletions src/entities/task/ui/Task.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const TaskProvider = ({
className={cn(
`relative min-h-38.5 overflow-hidden rounded-lg bg-white py-3.5 pr-5
pl-6 dark:bg-black`,
task.completed &&
'opacity-90 brightness-95 saturate-100 dark:brightness-125',
className
)}
{...props}>
Expand Down Expand Up @@ -77,7 +79,11 @@ const TaskTitle = ({ className, ...props }: ComponentProps<'p'>) => {

return (
<p
className={cn('mr-12 mb-2 truncate text-base font-semibold', className)}
className={cn(
'mb-2 max-w-60 truncate text-base font-semibold',
task.completed && 'text-black/40 line-through dark:text-white/40',
className
)}
{...props}>
{task.title}
</p>
Expand All @@ -93,6 +99,7 @@ const TaskDescription = ({ className, ...props }: ComponentProps<'p'>) => {
className={cn(
`text-md mb-3.5 line-clamp-2 max-w-68.75 text-balance text-ellipsis
text-black/70 dark:text-white/50`,
task.completed && 'text-black/40 dark:text-white/40',
className
)}
{...props}>
Expand Down Expand Up @@ -154,7 +161,12 @@ const TaskPriority = ({ className }: { className?: string }) => {
const { task } = useTaskContext()

return (
<div className={cn('mr-3.5', className)}>
<div
className={cn(
'mr-3.5',
task.completed && 'text-black/40 dark:text-white/40',
className
)}>
<p className='mb-1 text-xs text-black/50 dark:text-white/50'>Priority</p>
<div className='flex items-center gap-1'>
<div
Expand All @@ -174,7 +186,11 @@ const TaskDeadline = ({ className }: { className?: string }) => {

return (
task.deadline && (
<div className={cn(className)}>
<div
className={cn(
task.completed && 'text-black/40 dark:text-white/40',
className
)}>
<p className='mb-1 text-xs text-black/50 dark:text-white/50'>
Deadline
</p>
Expand Down
4 changes: 2 additions & 2 deletions src/entities/user/api/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const UserDtoSchema = v.object({
name: v.string(),
email: v.string(),
theme: v.picklist(THEMES),
avatar: v.nullable(v.string())
image: v.nullable(v.string())
})

export const EditUserDtoSchema = v.partial(
Expand All @@ -16,7 +16,7 @@ export const EditUserDtoSchema = v.partial(
email: v.pipe(v.string(), v.trim(), v.email()),
password: v.pipe(v.string(), v.trim(), v.minLength(8), v.maxLength(64)),
theme: v.picklist(THEMES),
avatar: v.pipe(
image: v.pipe(
v.instance(File),
v.mimeType(['image/jpg', 'image/jpeg', 'image/png', 'image/webp'])
)
Expand Down
8 changes: 6 additions & 2 deletions src/entities/user/api/queries.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { queryOptions } from '@tanstack/react-query'

import { userService } from './service'
import { authClient } from '@/shared/api'

export const userQueries = {
current: () => ['me'],
me: () =>
queryOptions({
queryKey: userQueries.current(),
queryFn: userService.getMe,
queryFn: async () => {
const session = await authClient.getSession()

return session?.user || null
},
staleTime: 1000 * 60 * 10, // 10 minutes
retry: false
})
Expand Down
2 changes: 1 addition & 1 deletion src/entities/user/model/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export const UserSchema = v.object({
id: v.string(),
name: v.string(),
email: v.string(),
avatar: v.nullable(v.string()),
image: v.nullable(v.string()),
theme: v.picklist(THEMES)
})
9 changes: 7 additions & 2 deletions src/features/user/change-theme/api/useChangeTheme.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { Theme } from '@/shared/config'

import { useMutation, useQueryClient } from '@tanstack/react-query'
import { parse } from 'valibot'

import { UserContracts, userQueries, userService } from '@/entities/user'
import { UserContracts, userQueries } from '@/entities/user'

import { authClient } from '@/shared/api'

export const useChangeTheme = () => {
const queryClient = useQueryClient()

return useMutation({
mutationFn: userService.editUser,
mutationFn: ({ theme }: { theme: Theme }) =>
authClient.updateUser({ theme }),
meta: { errorMessage: 'We couldn’t update your theme. Please try again' },
onMutate: async ({ theme }) => {
await queryClient.cancelQueries({ queryKey: userQueries.current() })
Expand Down
5 changes: 3 additions & 2 deletions src/features/user/edit-profile/ui/EditAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const EditAvatar = () => {
ref={ref}
accept='image/jpg, image/jpeg, image/png, image/webp'
className='hidden'
onChange={e => changeAvatar({ avatar: e.target.files?.[0] })}
onChange={e => changeAvatar({ image: e.target.files?.[0] })}
/>
{isPending ? (
<div
Expand All @@ -36,8 +36,9 @@ export const EditAvatar = () => {
className='focus-visible:styled-outline relative mx-auto mb-6 block
size-17'>
<img
src={user?.avatar || defaultAvatarUrl[resolveTheme(user?.theme)]}
src={user?.image || defaultAvatarUrl[resolveTheme(user?.theme)]}
alt='Avatar'
referrerPolicy='no-referrer'
className='size-[inherit] rounded-xl object-cover'
/>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ export const EditProfileDialogTrigger = () => {

const name = user?.name ?? 'Guest'

console.log(user)

return (
<DialogTrigger
//! TEMPORARY DISABLE BUTTON
disabled
type='button'
aria-label='Edit profile'
className='focus-visible:styled-outline flex items-center gap-2'>
<p>{name}</p>
<Avatar>
<AvatarImage
src={user?.avatar || defaultAvatarUrl[resolveTheme(user?.theme)]}
src={user?.image || defaultAvatarUrl[resolveTheme(user?.theme)]}
alt={name}
/>
<AvatarFallback>{name.charAt(0).toUpperCase()}</AvatarFallback>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/signin/api/useSigninUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const useSigninUser = (reset: UseFormReset<SigninSchema>) => {
mutationFn: sessionService.signin,
meta: {
errorMessage: e =>
e?.response?.status === 401
e?.status === 401
? 'The email or password you entered is incorrect. Please try again.'
: 'An error occurred during sign-in. Our technical team has been notified. Please try again shortly.'
},
Expand Down
6 changes: 3 additions & 3 deletions src/pages/signup/api/useSignupUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ export const useSignupUser = (reset: UseFormReset<SignupSchema>) => {
mutationFn: sessionService.signup,
meta: {
errorMessage: e =>
e?.response?.status === 409
e?.status === 422
? 'An account with this email address already exists. Please sign in or use a different email.'
: 'An error occurred during sign-up. Our technical team has been notified. Please try again shortly.'
},
onSuccess({ user }) {
onSuccess(data) {
reset()
queryClient.setQueryData(userQueries.current(), user)
queryClient.setQueryData(userQueries.current(), data?.user)
navigate({ to: '/dashboard' })
}
})
Expand Down
38 changes: 14 additions & 24 deletions src/pages/welcome/ui/SocialButton.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,21 @@
import { sessionApiEndpoints } from '@/entities/session'

import { env } from '@/shared/config'
import { capitalize } from '@/shared/lib'
import { Icon } from '@/shared/ui'

type SocialButtonProps = {
provider: 'google' | 'microsoft'
apiEndpoint: keyof typeof sessionApiEndpoints
onClick: () => void
}

export const SocialButton = ({ provider, apiEndpoint }: SocialButtonProps) => {
const handleClick = () => {
const url = env.VITE_API_BASE_URL + sessionApiEndpoints[apiEndpoint]
window.location.href = url
}

return (
<button
type='button'
className='flex w-84 items-center justify-center gap-2 rounded-lg bg-black
py-2.5 text-center text-white'
onClick={handleClick}>
<Icon
name={provider}
className='size-7 stroke-none'
/>
Continue with {capitalize(provider)}
</button>
)
}
export const SocialButton = ({ provider, onClick }: SocialButtonProps) => (
<button
type='button'
className='flex w-84 items-center justify-center gap-2 rounded-lg bg-black
py-2.5 text-center text-white'
onClick={onClick}>
<Icon
name={provider}
className='size-7 stroke-none'
/>
Continue with {capitalize(provider)}
</button>
)
Loading
Loading