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

feat: user name settings #649

Merged
merged 12 commits into from
Apr 26, 2024
6 changes: 3 additions & 3 deletions apps/api/src/user/model/user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export class CreateUserDto {
example: "admin",
description: "用户名不能为空,长度为2-20位",
})
@IsNotEmpty({ message: " 用户名不能为空" })
@IsNotEmpty({ message: "用户名不能为空" })
@Length(2, 20, { message: "用户名长度为2-20位" })
name: string;
username: string;

@ApiProperty({
example: "15512345678",
Expand All @@ -34,4 +34,4 @@ export class FindUserDto {
phone: string;
}

export class UpdateUserDto extends PickType(CreateUserDto, ["name"]) {}
export class UpdateUserDto extends PickType(CreateUserDto, ["username"]) {}
13 changes: 12 additions & 1 deletion apps/api/src/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Body, Controller, Get, Patch, UseGuards } from "@nestjs/common";
import { Body, Controller, Get, Patch, Post, UseGuards } from "@nestjs/common";

import { AuthGuard } from "../guards/auth.guard";
import { User, UserEntity } from "../user/user.decorators";
Expand All @@ -13,4 +13,15 @@ export class UserController {
updateInfo(@User() user: UserEntity, @Body() dto: UpdateUserDto) {
return this.userService.updateUser(user, dto);
}

// 给新用户第一次登录使用
// 目前使用 email 和 github 登录的用户 都不存在 username
// 所以这个接口有两个目的
// 1. 设置 username
// 2. 默认添加星荣的课程包到最近的课程包
@UseGuards(AuthGuard)
@Post("setup")
async initializeUser(@User() user: UserEntity, @Body("username") username: string) {
return this.userService.setup(user, username);
}
}
3 changes: 2 additions & 1 deletion apps/api/src/user/user.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Module } from "@nestjs/common";

import { LogtoModule } from "../logto/logto.module";
import { UserCourseProgressModule } from "../user-course-progress/user-course-progress.module";
import { UserController } from "./user.controller";
import { UserService } from "./user.service";

@Module({
imports: [LogtoModule],
imports: [LogtoModule, UserCourseProgressModule],
providers: [UserService],
controllers: [UserController],
exports: [UserService],
Expand Down
15 changes: 12 additions & 3 deletions apps/api/src/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Inject, Injectable } from "@nestjs/common";
import { HttpException, Inject, Injectable } from "@nestjs/common";

import { DB, DbType } from "../global/providers/db.provider";
import { LogtoService } from "../logto/logto.service";
import { UserCourseProgressService } from "../user-course-progress/user-course-progress.service";
import { UserEntity } from "../user/user.decorators";
import { UpdateUserDto } from "./model/user.dto";

Expand All @@ -10,6 +11,7 @@ export class UserService {
constructor(
@Inject(DB) private db: DbType,
private readonly logtoService: LogtoService,
private readonly userCourseProgressService: UserCourseProgressService,
) {}

async findUser(uId: string) {
Expand All @@ -24,8 +26,15 @@ export class UserService {
try {
const { data } = await this.logtoService.logtoApi.patch(`/api/users/${user.userId}`, dto);
return { data };
} catch (error) {
return undefined;
} catch (e) {
throw new HttpException(e.response.data.message, e.response.status);
}
}

async setup(user: UserEntity, username: string) {
await this.updateUser(user, { username });
// TODO coursePackId 和 courseId 先写死
await this.userCourseProgressService.upsert(user.userId, 1, 1, 0);
return true;
}
}
6 changes: 5 additions & 1 deletion apps/client/api/userInfo.ts → apps/client/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { type UserInfoResponse } from "@logto/vue";

import { http } from "./http";

export async function updateUserinfo(datas: Omit<Partial<UserInfoResponse>, "username">) {
export async function updateUserinfo(datas: Partial<UserInfoResponse>) {
return await http.patch<UserInfoResponse | undefined, UserInfoResponse | undefined>(
"/user",
datas,
);
}

export async function fetchUserSetup(username: string) {
return await http.post("/user/setup", { username });
}
12 changes: 6 additions & 6 deletions apps/client/components/DropMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ const showDropdown = ref(false);
const dropdownContainer = ref(null);
const GO_BACK_GAME_NAME = "goBackGamePage";
const MENU_OPTIONS = [
{
title: "返回游戏",
name: GO_BACK_GAME_NAME,
eventName: handleGoBackGamePage,
icon: "i-ph-game-controller",
},
// {
// title: "返回游戏",
// name: GO_BACK_GAME_NAME,
// eventName: handleGoBackGamePage,
// icon: "i-ph-game-controller",
// },
{
title: "设置",
name: "setting",
Expand Down
5 changes: 2 additions & 3 deletions apps/client/components/HttpErrorProvider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,15 @@ useHttpStatusError();
function useHttpStatusError() {
injectHttpStatusErrorHandler(async (errMessage, statusCode) => {
switch (statusCode) {
case 400:
Message.error(errMessage);
break;
case 401:
Message.error(errMessage, {
duration: 2000,
onLeave() {
signIn(window.location.pathname);
},
});
default:
Message.error(errMessage);
break;
}
});
Expand Down
32 changes: 31 additions & 1 deletion apps/client/components/Landing/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
<template>
<div class="container m-auto w-full font-customFont">
<!-- 提醒老用户需要重新去注册
1周后删除 -->
<div
role="alert"
class="alert alert-warning"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<span
>老用户需要重新注册!!! 全新版本已经到来 因为之前是 MVP 版本 所有数据已经删档
现在支持邮箱注册了哦 更保护您的隐私 感谢支持💗</span
>
<button
className="btn btn-sm text-purple-400"
@click="signIn()"
>
去注册
</button>
</div>
<LandingBanner @start-earthworm="startEarthworm" />
<LandingFeatures />
<LandingComments />
Expand All @@ -13,7 +43,7 @@
import { onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";

import { isAuthenticated } from "~/services/auth";
import { isAuthenticated, signIn } from "~/services/auth";
import { cancelShortcut, registerShortcut } from "~/utils/keyboardShortcuts";

const { startEarthworm } = useShortcutToGame();
Expand Down
4 changes: 2 additions & 2 deletions apps/client/components/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
class="logged-in flex items-center"
>
<div class="font-500 mx-2 max-w-[4em] truncate min-[500px]:max-w-[6em]">
{{ userStore.userNameGetter }}
{{ userStore.userInfo?.username }}
</div>
<DropMenu @update-show-modal="handleLogout" />
</div>
Expand All @@ -61,7 +61,7 @@
v-else
@click="signIn()"
aria-label="Login"
class="btn btn-ghost btn-sm mx-1 h-8 rounded-md px-4 text-base font-normal dark:text-white"
class="btn btn-ghost btn-sm mx-1 h-8 rounded-md bg-purple-500 px-4 text-base font-normal text-black hover:bg-purple-800 dark:text-white"
>
<span class="relative">登录</span>
</button>
Expand Down
42 changes: 12 additions & 30 deletions apps/client/components/user/Setting.vue
Original file line number Diff line number Diff line change
@@ -1,33 +1,5 @@
<template>
<div class="min-w-max space-y-8">
<section class="space-y-4">
<h2 class="text-lg font-medium">个人信息设置</h2>
<table class="table">
<tbody>
<tr class="hover">
<td class="label-text">昵称</td>
<td class="w-[300px] text-center">
<div class="join mr-12">
<input
class="join-item btn-sm"
type="text"
name="username"
pattern="请输入用户名称"
v-model="userName"
@keyup.enter="() => updateUserInfo(userName!)"
/>
<button
class="btn btn-outline btn-secondary btn-sm ml-1"
@click="() => updateUserInfo(userName!)"
>
更新
</button>
</div>
</td>
</tr>
</tbody>
</table>
</section>
<section class="space-y-4">
<h2 class="text-lg font-medium">游戏模式</h2>
<table class="table">
Expand Down Expand Up @@ -250,6 +222,7 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";

import Message from "~/components/main/Message/useMessage";
import { useAutoNextQuestion } from "~/composables/user/autoNext";
import { useErrorTip } from "~/composables/user/errorTip";
import { GameMode, useGameMode } from "~/composables/user/gameMode";
Expand All @@ -267,8 +240,17 @@ import { parseShortcutKeys } from "~/utils/keyboardShortcuts";

const dialogBoxRef = ref<HTMLElement | null>(null);
const userStore = useUserStore();
const updateUserInfo = userStore.updateUserInfo;
const userName = userStore.userNameGetter;
const nickname = ref(userStore.userNameGetter);
const handleUpdateNickname = async (event: KeyboardEvent) => {
const result = await userStore.updateUserInfo({
...userStore.userInfo!,
name: nickname.value,
});
if (result) {
(event.target as HTMLInputElement).blur();
Message.success("修改成功");
}
};
const { autoNextQuestion, toggleAutoQuestion } = useAutoNextQuestion();
const { keyboardSound, toggleKeyboardSound } = useKeyboardSound();
const { autoPlaySound, toggleAutoPlaySound } = useAutoPronunciation();
Expand Down
93 changes: 90 additions & 3 deletions apps/client/pages/callback.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,109 @@
<script setup lang="ts">
import { useHandleSignInCallback, useLogto } from "@logto/vue";
import { navigateTo } from "nuxt/app";
import { ref } from "vue";

import { fetchUserSetup } from "~/api/user";
import Message from "~/components/main/Message/useMessage";
import { getSignInCallback } from "~/services/auth";
import { useUserStore } from "~/store/user";

const userStore = useUserStore();
const logto = useLogto();
const { username, isShowSettingUsernameModal, handleChangeUsername } = useUsername();

const { isLoading } = useHandleSignInCallback(async () => {
const res = await logto.fetchUserInfo();
userStore.initUser(res!);

await navigateTo(getSignInCallback());
if (userStore.isNewUser()) {
// 让用户修改 username
isShowSettingUsernameModal.value = true;
} else {
await navigateTo(getSignInCallback());
}
});

function useUsername() {
const username = ref("");
const isShowSettingUsernameModal = ref(false);

async function handleChangeUsername() {
if (!checkUsername()) return;

await fetchUserSetup(username.value);
Message.success("用户名更新成功");
await navigateTo(getSignInCallback());
isShowSettingUsernameModal.value = false;
}

function checkUsername() {
const minLength = 2;
const errorMessage = {
empty: "用户名不能为空",
minLength: `用户名至少输入 ${minLength} 个字符`,
invalid: "用户名只能包含字母、数字和下划线,且首字符必须是字母或下划线",
};

if (!username.value) {
Message.error(errorMessage.empty);
return false;
}

if (username.value.length < minLength) {
Message.error(errorMessage.minLength);
return false;
}

const regex = /^[A-Za-z_]\w*$/;
if (!regex.test(username.value)) {
Message.error(errorMessage.invalid);
return false;
}

return true;
}

return {
checkUsername,
username,
isShowSettingUsernameModal,
handleChangeUsername,
};
}
</script>

<template>
<!-- When it's working in progress -->
<p v-if="isLoading">Redirecting...</p>
<div class="flex w-full flex-col pt-2">
<template v-if="isLoading && !isShowSettingUsernameModal">
<Loading></Loading>
</template>
<template v-else>
<dialog
class="modal"
:open="true"
>
<div class="modal-box">
<h3 class="mb-4 text-lg font-bold">设置用户名</h3>
<input
v-model="username"
type="text"
placeholder="请输入用户名"
class="input input-bordered input-sm w-full"
maxlength="20"
@keydown.enter="handleChangeUsername"
/>
<div class="modal-action">
<button
class="btn btn-primary"
type="submit"
@click="handleChangeUsername"
>
确定
</button>
</div>
</div>
</dialog>
</template>
</div>
</template>
Loading