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

Restart Challenge #37

Open
wants to merge 1 commit into
base: development
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions src/app/api/restart-challenge/restart-challenge-schema.ts
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(),
});
38 changes: 38 additions & 0 deletions src/app/api/restart-challenge/route.ts
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);
Copy link
Contributor

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.


const user = await auth.loadSessionUser();
// impossible that frontend and cookie issue.
if (!user || user.uid !== userId) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 });
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

}
}
81 changes: 81 additions & 0 deletions src/components/button/button.module.scss
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button component has been moved to src/components/utils/button, so all the folder src/components/button and all its contents can safely be deleted.

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%;
}
27 changes: 27 additions & 0 deletions src/components/button/button.tsx
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;
1 change: 1 addition & 0 deletions src/components/button/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Button } from './button';
43 changes: 43 additions & 0 deletions src/contexts/user-context/restart-challenge-modal.tsx
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) {}
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
);
}
15 changes: 13 additions & 2 deletions src/services/server/user-repository/supabase-user-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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();

Expand Down
1 change: 1 addition & 0 deletions src/services/server/user-repository/user-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface UserRepository {
makeHybrid(userId: string): Promise<User>;
awardRegisterToVoteBadge(userId: string): Promise<User>;
awardElectionRemindersBadge(userId: string): Promise<User>;
restartChallenge(userId: string): Promise<number>;
}
57 changes: 57 additions & 0 deletions src/stories/components/restart-challenge/restart.stories.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
);
},
};

Loading