-
Notifications
You must be signed in to change notification settings - Fork 4
feat: redesign profile pages and fix activity create layout #27
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
base: main
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 | ||||
|---|---|---|---|---|---|---|
| @@ -1 +1,4 @@ | ||||||
| NEXT_PUBLIC_API_HOST = http://127.0.0.1:8080 | ||||||
|
|
||||||
| # Skip OAuth proxy for local development (proxy server may be unavailable) | ||||||
| SKIP_OAUTH_PROXY = 1 | ||||||
|
Comment on lines
+3
to
+4
Member
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.
Suggested change
不要跳过代理,它是为了中国大多数电脑即使挂梯子也无法让 Node.js 稳定访问 GitHub OAuth 接口而架设的。只是从“闭源社”迁回时没部署我们自己的测试服,但现在我搞好了。 |
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -26,6 +26,35 @@ Open-source [Hackathon][1] Platform with **Git-based Cloud Development Environme | |||||
| - PWA framework: [Workbox v6][9] | ||||||
| - CI / CD: GitHub [Actions][10] + [Vercel][11] | ||||||
|
|
||||||
| ## Environment Configuration | ||||||
|
|
||||||
| Copy `.env` to `.env.local` and configure the following required variables: | ||||||
|
|
||||||
| ```bash | ||||||
| # GitHub OAuth (required for login) | ||||||
| GITHUB_OAUTH_CLIENT_ID=your_client_id | ||||||
| GITHUB_OAUTH_CLIENT_SECRET=your_client_secret | ||||||
|
|
||||||
| # JWT Secret (required for session) | ||||||
| JWT_SECRET=your_jwt_secret | ||||||
|
|
||||||
| # API Host | ||||||
| NEXT_PUBLIC_API_HOST=https://openhackathon-service.onrender.com | ||||||
|
Comment on lines
+41
to
+42
Member
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.
Suggested change
公开变量在 |
||||||
|
|
||||||
| # Skip OAuth proxy for local development (optional, already set in .env.development) | ||||||
| SKIP_OAUTH_PROXY=1 | ||||||
| ``` | ||||||
|
|
||||||
| ### Creating GitHub OAuth App | ||||||
|
|
||||||
| 1. Go to [GitHub Developer Settings](https://github.com/settings/developers) | ||||||
| 2. Click "New OAuth App" | ||||||
| 3. Fill in: | ||||||
| - **Application name**: HOP Local Dev | ||||||
| - **Homepage URL**: `http://localhost:3000` | ||||||
| - **Authorization callback URL**: `http://localhost:3000/login` | ||||||
|
Member
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.
Suggested change
写死子路径会让 |
||||||
| 4. Copy the Client ID and generate a Client Secret | ||||||
|
|
||||||
| ## Getting Started | ||||||
|
|
||||||
| First, run the development server: | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| import { Icon } from 'idea-react'; | ||
| import { observer } from 'mobx-react'; | ||
| import { useRouter } from 'next/router'; | ||
| import { useContext } from 'react'; | ||
| import { Button, Dropdown } from 'react-bootstrap'; | ||
|
|
||
|
|
@@ -9,33 +11,41 @@ import LanguageMenu from './LanguageMenu'; | |
| const UserBar = observer(() => { | ||
| const { t } = useContext(I18nContext), | ||
| { user } = sessionStore; | ||
| const router = useRouter(); | ||
|
|
||
| const showName = user?.name || user?.email || user?.mobilePhone || ''; | ||
| const loginUrl = `/login?redirect=${encodeURIComponent(router.asPath)}`; | ||
|
|
||
| return ( | ||
| <> | ||
| <Button variant="success" href="/activity/create"> | ||
| {t('create_hackathons')} | ||
| </Button> | ||
|
|
||
| {user && ( | ||
| {user ? ( | ||
| <Dropdown> | ||
| <Dropdown.Toggle>{showName}</Dropdown.Toggle> | ||
| <Dropdown.Menu> | ||
| <Dropdown.Item href="/me">{t('profile')}</Dropdown.Item> | ||
| <Dropdown.Item href={`/user/${user.id}`}>{t('home_page')}</Dropdown.Item> | ||
|
Comment on lines
+29
to
30
Member
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. 这两项功能重复,请直接在原有用户详情页上改。 |
||
| <Dropdown.Item | ||
| title={t('edit_profile_tips')} | ||
| target="_blank" | ||
| href="https://github.com/settings/profile" | ||
| onClick={() => sessionStore.signOut(true)} | ||
| > | ||
| {t('edit_profile')} | ||
| </Dropdown.Item> | ||
| <Dropdown.Divider /> | ||
| <Dropdown.Item onClick={() => sessionStore.signOut(true)}> | ||
| {t('sign_out')} | ||
| </Dropdown.Item> | ||
| </Dropdown.Menu> | ||
| </Dropdown> | ||
| ) : ( | ||
| <Button variant="outline-light" href={loginUrl}> | ||
| <Icon name="github" className="me-2" /> | ||
| {t('sign_in')} | ||
| </Button> | ||
| )} | ||
| <LanguageMenu /> | ||
| </> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| # HOP 开发任务清单(MVP → 完整网站) | ||
|
|
||
| 本文档用于把现有功能串联成可交付的网站,并将工作拆成可执行的任务列表。 | ||
|
|
||
| - 当前工作分支:`feat/auth` | ||
| - 后端:已存在(前端以 API 集成为主) | ||
|
|
||
| --- | ||
|
|
||
| ## 0. 现状速览(基于代码盘点) | ||
|
|
||
| - 已有页面:`/`、`/activity`、`/activity/[name]`、`/activity/create`(受保护) | ||
| - 已有鉴权链路雏形:`pages/api/core.ts`(`githubOAuth2` + `jwtSigner` + `sessionGuard`) | ||
| - 已有会话模型:`models/User/Session.ts`(`getProfile()`、`signInWithGitHub()`、`signOut()`) | ||
| - 已有参赛/团队模型:`models/Activity/*`(`signOne()`、`team.joinTeam()` 等) | ||
|
|
||
| --- | ||
|
|
||
| ## 1. MVP 目标(验收口径) | ||
|
|
||
| ### 1.1 参赛者旅程(MVP) | ||
|
|
||
| - 能浏览 hackathon 列表与详情 | ||
| - 能完成 GitHub 登录 | ||
| - 能在详情页报名(Join/Sign up)并能看到报名状态 | ||
| - 能在「我的参赛」里看到自己报名的活动与状态 | ||
|
|
||
| ### 1.2 主办方旅程(MVP) | ||
|
|
||
| - 能完成 GitHub 登录 | ||
| - 能创建 hackathon | ||
| - 创建成功后可跳转到该活动详情/管理入口 | ||
| - 能在「我发起的」里看到自己创建的活动 | ||
|
|
||
| --- | ||
|
|
||
| ## 2. 里程碑与任务清单 | ||
|
|
||
| > 说明:每个任务包含「验收标准」与「主要影响范围」。 | ||
|
|
||
| ### Milestone A:鉴权与登录态(对应分支 `feat/auth`,优先做) | ||
|
|
||
| #### A1. 顶部导航增加“登录/退出/个人入口” | ||
|
|
||
| - 优先级:P0 | ||
| - 验收标准: | ||
| - 未登录:导航显示“使用 GitHub 登录”按钮 | ||
| - 已登录:显示用户头像/用户名 + “退出”按钮 + “个人中心”入口 | ||
| - 退出后:cookie 被清理(`token`/`JWT`)且 UI 回到未登录态 | ||
| - 主要影响范围: | ||
| - `components/layout/MainNavigation.*` | ||
| - `models/User/Session.ts` | ||
|
|
||
| #### A2. 新增个人中心最小页面 `/me` | ||
|
|
||
| - 优先级:P0 | ||
| - 验收标准: | ||
| - 已登录访问 `/me`:展示用户基本信息(至少用户名、邮箱、头像) | ||
| - 未登录访问 `/me`:引导登录(跳转或提示) | ||
| - 主要影响范围: | ||
| - `pages/me.tsx`(新)或 `pages/user/me.tsx`(按现有结构定) | ||
| - 复用 `sessionGuard` 或前端登录态判断 | ||
|
|
||
| #### A3. 会话初始化:全站启动时拉取 profile(可缓存) | ||
|
|
||
| - 优先级:P0 | ||
| - 验收标准: | ||
| - 刷新页面后仍能正确恢复登录态 | ||
| - 如果 cookie 有 `JWT`,能拉到 `user/session` 并在导航展示 | ||
| - 如果 `JWT` 失效,能回到未登录态并提示一次即可 | ||
| - 主要影响范围: | ||
| - `pages/_app.tsx` | ||
| - `models/User/Session.ts` | ||
|
|
||
| #### A4. 统一 401/403 体验(最小版本) | ||
|
|
||
| - 优先级:P1 | ||
| - 验收标准: | ||
| - API 返回 401:统一提示“需要登录”并提供跳转/按钮 | ||
| - 受保护页面:未登录时不出现白屏/静默失败 | ||
| - 主要影响范围: | ||
| - `models/User/Session.ts` 的 HTTPClient middleware(或公共 client) | ||
| - 相关页面(`/activity/create`、未来的 `/me/*`) | ||
|
|
||
| #### A5. 环境变量与 OAuth 配置校验 | ||
|
|
||
| - 优先级:P1 | ||
| - 验收标准: | ||
| - 缺少 `GITHUB_OAUTH_CLIENT_ID/SECRET` 时给出明确报错(开发期) | ||
| - README 或 docs 中说明必须配置项 | ||
| - 主要影响范围: | ||
| - `pages/api/core.ts` | ||
| - `README.md` 或本 docs | ||
|
|
||
| --- | ||
|
|
||
| ### Milestone B:参赛闭环(报名 → 状态 → 我的参赛) | ||
|
|
||
| #### B1. 详情页报名动作与状态刷新 | ||
|
|
||
| - 优先级:P0 | ||
| - 验收标准: | ||
| - `/activity/[name]` 点击报名:调用 `activityStore.signOne(name)` 成功 | ||
| - 成功后页面状态刷新:能展示 pending/approved/rejected 等状态 | ||
| - 报名窗口期外:按钮不可点击或不显示(按现有逻辑) | ||
| - 主要影响范围: | ||
| - `pages/activity/[name]/index.tsx` | ||
| - `models/Activity/index.ts` | ||
|
|
||
| #### B2. 新增「我的参赛」页 `/me/registrations` | ||
|
|
||
| - 优先级:P0 | ||
| - 验收标准: | ||
| - 能列出当前用户的报名记录(至少活动名、状态、链接) | ||
| - 空态友好(无报名时提示去活动列表) | ||
| - 主要影响范围: | ||
| - `pages/me/registrations.tsx`(新) | ||
| - 可能新增对应 model 方法(取决于后端已有 API) | ||
|
|
||
| #### B3. 团队相关:approved 后引导创建/加入队伍 | ||
|
|
||
| - 优先级:P1 | ||
| - 验收标准: | ||
| - 状态 approved 且活动期间:显示“创建队伍”或“加入队伍”入口 | ||
| - 加入/退出队伍操作成功后能更新当前队伍状态 | ||
| - 主要影响范围: | ||
| - `pages/activity/[name]/index.tsx` | ||
| - `models/Activity/Team.ts` + `components/Team/*` | ||
|
|
||
| --- | ||
|
|
||
| ### Milestone C:发起闭环(创建 → 我发起的 → 管理入口) | ||
|
|
||
| #### C1. 创建活动成功后的跳转与提示 | ||
|
|
||
| - 优先级:P0 | ||
| - 验收标准: | ||
| - `/activity/create` 提交成功后:跳转到新活动详情 `/activity/[name]`(或管理页) | ||
| - 失败时:展示具体错误原因(字段校验/权限等) | ||
| - 主要影响范围: | ||
| - `components/Activity/ActivityEditor.tsx` | ||
| - `models/Activity/index.ts` | ||
| - `pages/activity/create.tsx` | ||
|
|
||
| #### C2. 新增「我发起的」页 `/me/hackathons` | ||
|
|
||
| - 优先级:P1 | ||
| - 验收标准: | ||
| - 能列出当前用户创建的活动 | ||
| - 每项可进入详情/管理入口 | ||
| - 主要影响范围: | ||
| - `pages/me/hackathons.tsx`(新) | ||
| - 依赖后端是否提供“按用户过滤”的 API | ||
|
|
||
| #### C3. 最小管理入口(可先只读) | ||
|
|
||
| - 优先级:P2 | ||
| - 验收标准: | ||
| - 至少存在 `/activity/[name]/manage` 页面骨架 | ||
| - 受保护 + 权限校验(不是主办方则提示无权限) | ||
| - 主要影响范围: | ||
| - `pages/activity/[name]/manage/*`(如不存在则新建) | ||
|
|
||
| --- | ||
|
|
||
| ## 3. 分支与 PR 拆分建议 | ||
|
|
||
| - `feat/auth`(当前):Milestone A(A1-A5) | ||
| - 后续建议按功能拆分: | ||
| - `feat/registration-flow`:B1 + B2 | ||
| - `feat/team-flow`:B3 | ||
| - `feat/activity-create-flow`:C1 | ||
| - `feat/organizer-pages`:C2 + C3 | ||
|
|
||
| --- | ||
|
|
||
| ## 4. 开发约定(最小) | ||
|
|
||
| - 任何受保护页面优先使用现有 `sessionGuard`(SSR)或统一的登录态保护组件。 | ||
| - API_HOST: | ||
| - 生产:`.env` 指向远端 | ||
| - 开发:如果不启动本地后端,请将 `.env.development` 改为远端或实现自动 fallback(后续可单独任务处理)。 | ||
|
|
||
| --- | ||
|
|
||
| ## 5. 待确认(用于把任务变成“可直接开工”的更细粒度 issue) | ||
|
|
||
| - 后端是否已有: | ||
| - “我的报名列表”接口? | ||
| - “我发起的活动列表”接口? | ||
| - 组织方/管理员权限字段如何判断?(roles 还是 organizerId) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,23 +1,39 @@ | ||||||||||||||||||||
| import { Base, User } from '@freecodecamp-chengdu/hop-service'; | ||||||||||||||||||||
| import { HTTPClient } from 'koajax'; | ||||||||||||||||||||
| import { HTTPClient, HTTPError } from 'koajax'; | ||||||||||||||||||||
| import { computed, observable } from 'mobx'; | ||||||||||||||||||||
| import { BaseModel, persist, restore, toggle } from 'mobx-restful'; | ||||||||||||||||||||
| import { buildURLData, setCookie } from 'web-utility'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import { API_HOST, isServer, JWT, token } from '../../configuration'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export const ownClient = new HTTPClient({ baseURI: API_HOST, responseType: 'json' }).use( | ||||||||||||||||||||
| ({ request }, next) => { | ||||||||||||||||||||
| export const ownClient = new HTTPClient({ baseURI: API_HOST, responseType: 'json' }) | ||||||||||||||||||||
| .use(({ request }, next) => { | ||||||||||||||||||||
| if (JWT) request.headers = { ...request.headers, Authorization: `Bearer ${JWT}` }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return next(); | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| .use(async ({ request }, next) => { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| return await next(); | ||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||
| if (error instanceof HTTPError) { | ||||||||||||||||||||
| const { status } = error.response; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (status === 401 || status === 403) { | ||||||||||||||||||||
| if (!isServer()) { | ||||||||||||||||||||
| const currentPath = globalThis.location?.pathname || '/'; | ||||||||||||||||||||
| const loginUrl = `/login?redirect=${encodeURIComponent(currentPath)}`; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| globalThis.location?.assign(loginUrl); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| throw error; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }); | ||||||||||||||||||||
|
Comment on lines
+15
to
+33
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. 🧩 Analysis chain🏁 Script executed: find . -type f -name "Session.ts" | head -20Repository: FreeCodeCamp-Chengdu/HOP Length of output: 92 🏁 Script executed: git ls-files | grep -E "(login|auth)" | grep -E "\.(ts|tsx|js)$" | head -20Repository: FreeCodeCamp-Chengdu/HOP Length of output: 83 🏁 Script executed: fd -e ts -e tsx | grep -i session | head -20Repository: FreeCodeCamp-Chengdu/HOP Length of output: 90 🏁 Script executed: cat -n models/User/Session.tsRepository: FreeCodeCamp-Chengdu/HOP Length of output: 3357 🏁 Script executed: cat -n pages/login.tsxRepository: FreeCodeCamp-Chengdu/HOP Length of output: 1385 🏁 Script executed: rg -n "sessionGuard" pages/api/core.ts pages/api/core.js -A 20Repository: FreeCodeCamp-Chengdu/HOP Length of output: 254 🏁 Script executed: fd -e ts -e js | xargs grep -l "sessionGuard" | head -10Repository: FreeCodeCamp-Chengdu/HOP Length of output: 85 🏁 Script executed: cat -n pages/api/core.tsRepository: FreeCodeCamp-Chengdu/HOP Length of output: 4172 🏁 Script executed: rg -n "ownClient|client\." pages/login.tsxRepository: FreeCodeCamp-Chengdu/HOP Length of output: 50 🏁 Script executed: rg -n "fetch\|axios\|ownClient" pages/login.tsx pages/api/core.ts | grep -E "(fetch|axios|ownClient)"Repository: FreeCodeCamp-Chengdu/HOP Length of output: 50 🏁 Script executed: rg -n "location.assign|location.href" --type ts --type tsx | head -20Repository: FreeCodeCamp-Chengdu/HOP Length of output: 95 🏁 Script executed: rg -n "401|403" models/User/Session.ts pages/login.tsx pages/api/core.tsRepository: FreeCodeCamp-Chengdu/HOP Length of output: 141 The error re-throw after redirect is unnecessary but has minimal impact. Line 31 throws the error even after initiating the redirect on line 27. Since Regarding infinite redirects: The login page is protected by server-side middleware ( 🤖 Prompt for AI Agents
Comment on lines
+15
to
+33
Member
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.
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| export interface SessionUser | ||||||||||||||||||||
| extends Base, | ||||||||||||||||||||
| Record<'username' | 'email', string>, | ||||||||||||||||||||
| Record<'confirmed' | 'blocked', boolean> { | ||||||||||||||||||||
| extends Base, Record<'username' | 'email', string>, Record<'confirmed' | 'blocked', boolean> { | ||||||||||||||||||||
| provider: 'local' | 'github'; | ||||||||||||||||||||
| gender?: 'Female' | 'Male' | 'Other'; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,8 @@ import Link from 'next/link'; | |
| import { Col, Container, Image, Row } from 'react-bootstrap'; | ||
|
|
||
| import { MainNavigation } from '../components/layout/MainNavigation'; | ||
| import { isServer } from '../configuration'; | ||
| import { isServer, JWT } from '../configuration'; | ||
| import sessionStore from '../models/User/Session'; | ||
| import { | ||
| createI18nStore, | ||
| i18n, | ||
|
|
@@ -45,6 +46,19 @@ export default class CustomApp extends App<I18nProps> { | |
|
|
||
| if (tips) alert(tips); | ||
| }); | ||
|
|
||
| this.initSession(); | ||
| } | ||
|
|
||
| async initSession() { | ||
| if (!JWT) return; | ||
|
|
||
| try { | ||
| await sessionStore.getProfile(); | ||
| } catch (error) { | ||
| console.error('Session restore failed:', error); | ||
| sessionStore.signOut(); | ||
| } | ||
|
Comment on lines
+49
to
+61
Member
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. 不要在每个页面上检测登录状态,会拖慢很多不需要登录态的公开页面。哪个页面需要,哪个页面自己检查登录态。 |
||
| } | ||
|
|
||
| render() { | ||
|
|
||
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.
Remove spaces around the equal sign.
Environment variable assignments should not have spaces around the
=operator, as some parsers may not handle them correctly.Apply this diff:
📝 Committable suggestion
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 4-4: [SpaceCharacter] The line has spaces around equal sign
(SpaceCharacter)
🤖 Prompt for AI Agents