-
Notifications
You must be signed in to change notification settings - Fork 9
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
Restart Challenge #37
base: development
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { z } from 'zod'; | ||
|
||
export const restartChallengeSchema = z.object({ | ||
userId: z.string().uuid(), | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import 'server-only'; | ||
import { NextResponse, type NextRequest } from 'next/server'; | ||
import { restartChallengeSchema } from './restart-challenge-schema'; | ||
import { serverContainer } from '@/services/server/container'; | ||
import { SERVER_SERVICE_KEYS } from '@/services/server/keys'; | ||
import { ServerError } from '@/errors/server-error'; | ||
|
||
export async function PUT(request: NextRequest) { | ||
const auth = serverContainer.get(SERVER_SERVICE_KEYS.Auth); | ||
const userRepository = serverContainer.get(SERVER_SERVICE_KEYS.UserRepository); | ||
|
||
try { | ||
const data = await request.json(); | ||
const { userId } = restartChallengeSchema.parse(data); | ||
|
||
const user = await auth.loadSessionUser(); | ||
// impossible that frontend and cookie issue. | ||
if (!user || user.uid !== userId) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^ Please remove vestigial comments such as this one and the one on line 24 |
||
return NextResponse.json( | ||
{ error: 'Unauthorized' }, | ||
{ status: 401 }, | ||
); | ||
} | ||
//sign in so no issue in the user id . please improve it!!!!!! | ||
const newTimestamp = await userRepository.restartChallenge(userId); | ||
|
||
return NextResponse.json( | ||
{ challengeEndTimestamp: newTimestamp }, | ||
{ status: 200 }, | ||
); | ||
} catch (e) { | ||
if (e instanceof ServerError) { | ||
return NextResponse.json({ error: e.message }, { status: e.statusCode }); | ||
} | ||
|
||
return NextResponse.json({ error: 'Bad data.' }, { status: 400 }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because this route will no longer be parsing a request body, this catch all statement should probably be a 500 error code with a more general error message. |
||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The button component has been moved to |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
// button.module.scss | ||
@use '../../styles/partials/variables/font-families'; | ||
@use '../../styles/partials/variables/font-weights'; | ||
@use '../../styles/partials/variables/letter-spacings'; | ||
@use '../../styles/partials/variables/font-sizes'; | ||
@use '../../styles/partials/variables/colors'; | ||
@use '../../styles/partials/variables/gradients'; | ||
|
||
@mixin -base { | ||
position: relative; | ||
display: inline-block; | ||
font-family: font-families.$bebas-neue; | ||
font-weight: font-weights.$regular; | ||
letter-spacing: letter-spacings.$large; | ||
text-align: center; | ||
text-decoration: none; | ||
text-transform: uppercase; | ||
border-style: solid; | ||
border-radius: 40px; | ||
border-width: 4px; | ||
border-color: colors.$black-8by8; | ||
} | ||
|
||
@mixin -outline { | ||
&::after { | ||
content: ""; | ||
position: absolute; | ||
border-radius: 40px; | ||
top: -8px; | ||
left: -8px; | ||
right: -8px; | ||
bottom: -8px; | ||
border: colors.$white 4px solid; | ||
} | ||
} | ||
|
||
@mixin -gradient-text { | ||
:first-child { | ||
// a span element should be added inside the button to contain the text, these styles will apply to it | ||
background: gradients.$yellow-teal; | ||
background-clip: text; | ||
-webkit-background-clip: text; | ||
-moz-background-clip: text; | ||
-webkit-text-fill-color: transparent; | ||
-moz-text-fill-color: transparent; | ||
color: transparent; | ||
font-size: inherit; | ||
} | ||
} | ||
|
||
.btn_gradient { | ||
@include -base; | ||
@include -outline; | ||
background: gradients.$yellow-teal; | ||
color: colors.$black-8by8; | ||
} | ||
.custom-font-size { | ||
font-size: 20px; // 你可以根据需要调整字体大小 | ||
} | ||
.btn_inverted { | ||
@include -base; | ||
@include -gradient-text; | ||
@include -outline; | ||
background: colors.$black-8by8; | ||
} | ||
|
||
.btn_lg { | ||
font-size: font-sizes.$lg; | ||
height: 64px; | ||
border-radius: 40px; | ||
border-width: 4px; | ||
} | ||
|
||
.btn_sm { | ||
font-size: font-sizes.$md; | ||
padding: 4px 14px; | ||
} | ||
|
||
.btn_wide { | ||
width: 100%; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// Button.tsx | ||
import React, { FC } from 'react'; | ||
import styles from './button.module.scss'; | ||
|
||
type ButtonProps = { | ||
variant?: 'btn_gradient' | 'btn_inverted'; | ||
size?: 'btn_lg' | 'btn_sm'; | ||
wide?: boolean; | ||
children: React.ReactNode; | ||
}; | ||
|
||
const Button: FC<ButtonProps> = ({ variant = 'btn_gradient', size = 'btn_lg', wide = false, children }) => { | ||
const classNames = [styles[variant], styles[size]]; | ||
if (wide) { | ||
classNames.push(styles.btn_wide); | ||
} | ||
|
||
console.log('Button classNames:', classNames); // Debugging line | ||
|
||
return ( | ||
<button className={classNames.join(' ')}> | ||
<span>{children}</span> | ||
</button> | ||
); | ||
}; | ||
|
||
export default Button; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as Button } from './button'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
"use client"; | ||
import { Modal } from "@/components/utils/modal"; | ||
import { Button } from "@/components/utils/button"; | ||
import { useContextSafely } from "@/hooks/use-context-safely"; | ||
import { UserContext } from "@/contexts/user-context"; | ||
import { calculateDaysRemaining } from "@/app/progress/calculate-days-remaining"; | ||
import { useState } from "react"; | ||
|
||
export function RestartChallengeModal() { | ||
const [isLoading, setLoading] = useState(false); | ||
|
||
const { restartChallenge, user } = useContextSafely(UserContext, "RestartChallengeModal"); | ||
if (!user) return null; | ||
const showModal = calculateDaysRemaining(user) === 0; | ||
const restartAndChangeisLoading = async () => { | ||
try{ | ||
setLoading(true); | ||
await restartChallenge(); | ||
} catch (error) {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe use the AlertsContext to show the user an error message if this fails? |
||
finally { | ||
setLoading(false); | ||
} | ||
} | ||
return ( | ||
<Modal ariaLabel="Restart Challenge" theme="dark" isOpen={showModal} closeModal={() => {}}> | ||
{isLoading ? ( | ||
<p>Restarting your challenge...</p> | ||
) : ( | ||
<> | ||
<p>Oops, times up! But no worries, restart your challenge to continue!</p> | ||
|
||
<Button | ||
onClick={restartAndChangeisLoading} | ||
size="sm" | ||
style={{ marginTop: '16px' }} // 你可以根据需要调整这个值 | ||
> | ||
Restart Challenge | ||
</Button> | ||
</> | ||
)} | ||
</Modal> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ import type { UserRepository } from './user-repository'; | |
import type { User } from '@/model/types/user'; | ||
import type { CreateSupabaseClient } from '../create-supabase-client/create-supabase-client'; | ||
import type { IUserRecordParser } from '../user-record-parser/i-user-record-parser'; | ||
|
||
import { DateTime } from 'luxon'; | ||
/** | ||
* An implementation of {@link UserRepository} that interacts with | ||
* a [Supabase](https://supabase.com/) database and parses rows returned from | ||
|
@@ -25,7 +25,18 @@ export const SupabaseUserRepository = inject( | |
private createSupabaseClient: CreateSupabaseClient, | ||
private userRecordParser: IUserRecordParser, | ||
) {} | ||
|
||
async restartChallenge(userId: string): Promise<number> { | ||
const supabase = this.createSupabaseClient(); | ||
const updatedChallengeEndTimestamp = DateTime.now().plus({ days: 8 }).toUnixInteger(); | ||
const { error } = await supabase.from('users').update({ challenge_end_timestamp: updatedChallengeEndTimestamp }).eq('id', userId); | ||
|
||
if(error) { | ||
throw new ServerError('Failed to update user.', 500); | ||
} | ||
|
||
return updatedChallengeEndTimestamp; | ||
//follow the style of getUserById AND supabase->throw an error | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^ Please remove this comment. |
||
} | ||
async getUserById(userId: string): Promise<User | null> { | ||
const supabase = this.createSupabaseClient(); | ||
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you tidy up the vertical whitespace in this file, please? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { Meta, StoryObj } from '@storybook/react'; | ||
import { GlobalStylesProvider } from '@/stories/global-styles-provider'; | ||
import { RestartChallengeModal } from '@/contexts/user-context/restart-challenge-modal'; | ||
import { Builder } from 'builder-pattern'; | ||
import { UserContext,type UserContextType } from '@/contexts/user-context'; | ||
import type { User } from '@/model/types/user'; | ||
import { PropsWithChildren, useState } from 'react'; | ||
import { time } from 'console'; | ||
import { promise } from 'zod'; | ||
import { DateTime } from 'luxon'; | ||
const meta: Meta<typeof RestartChallengeModal> = { | ||
component: RestartChallengeModal, | ||
}; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof RestartChallengeModal>; | ||
|
||
const createUserContext = () => { | ||
const user: User = Builder<User>().uid("1").challengeEndTimestamp(0).build(); | ||
}; | ||
function UserContextProvider({ children } : PropsWithChildren) { | ||
const [user, setUser] = useState<User>(Builder<User>().uid("1").challengeEndTimestamp(0).build()); | ||
const restartChallenge = async () => { | ||
return new Promise <void >(resolve=>{ | ||
setTimeout(() => { | ||
setUser(Builder<User>().uid("1").challengeEndTimestamp(DateTime.now().plus({days: 8}).toMillis()).build()); | ||
resolve(); | ||
},3000); }); | ||
}; | ||
return ( | ||
<UserContext.Provider value={Builder<UserContextType>() | ||
.user(user) | ||
.restartChallenge(restartChallenge) | ||
.build()} | ||
> | ||
{children} | ||
</UserContext.Provider> | ||
); | ||
|
||
|
||
} | ||
export const RestartChallenge: Story = { | ||
|
||
render: () => { | ||
|
||
|
||
return ( | ||
<GlobalStylesProvider> | ||
<UserContextProvider> | ||
<RestartChallengeModal /> | ||
</UserContextProvider> | ||
</GlobalStylesProvider> | ||
); | ||
}, | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^ I don't think this route actualy needs a schema, or to read any data from the request body, for that matter. I think you can just use the Auth class to get the user from cookies.