Parcourir la source

add Login.vue

tomoya il y a 2 ans
Parent
commit
9cdcb4931a

+ 75 - 0
frontend/package-lock.json

@@ -12,6 +12,7 @@
         "pinia": "^2.0.28",
         "sass": "^1.57.1",
         "vue": "^3.2.45",
+        "vue-i18n": "^9.2.2",
         "vue-router": "^4.1.6",
         "vuetify": "^3.1.1"
       },
@@ -919,6 +920,63 @@
       "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
       "dev": true
     },
+    "node_modules/@intlify/core-base": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.2.2.tgz",
+      "integrity": "sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==",
+      "dependencies": {
+        "@intlify/devtools-if": "9.2.2",
+        "@intlify/message-compiler": "9.2.2",
+        "@intlify/shared": "9.2.2",
+        "@intlify/vue-devtools": "9.2.2"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/@intlify/devtools-if": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.2.2.tgz",
+      "integrity": "sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==",
+      "dependencies": {
+        "@intlify/shared": "9.2.2"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/@intlify/message-compiler": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz",
+      "integrity": "sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==",
+      "dependencies": {
+        "@intlify/shared": "9.2.2",
+        "source-map": "0.6.1"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/@intlify/shared": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
+      "integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/@intlify/vue-devtools": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz",
+      "integrity": "sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==",
+      "dependencies": {
+        "@intlify/core-base": "9.2.2",
+        "@intlify/shared": "9.2.2"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
@@ -5571,6 +5629,23 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "dev": true
     },
+    "node_modules/vue-i18n": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.2.2.tgz",
+      "integrity": "sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==",
+      "dependencies": {
+        "@intlify/core-base": "9.2.2",
+        "@intlify/shared": "9.2.2",
+        "@intlify/vue-devtools": "9.2.2",
+        "@vue/devtools-api": "^6.2.1"
+      },
+      "engines": {
+        "node": ">= 14"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
     "node_modules/vue-router": {
       "version": "4.1.6",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",

+ 1 - 0
frontend/package.json

@@ -16,6 +16,7 @@
     "pinia": "^2.0.28",
     "sass": "^1.57.1",
     "vue": "^3.2.45",
+    "vue-i18n": "^9.2.2",
     "vue-router": "^4.1.6",
     "vuetify": "^3.1.1"
   },

+ 1 - 1
frontend/src/App.vue

@@ -10,7 +10,7 @@
 import { RouterView } from "vue-router";
 import { useMainStore } from "@/stores/main";
 import { onMounted } from "vue";
-import LoadingView from "./views/LoadingView.vue";
+import LoadingView from "@/components/Loading.vue";
 import { storeToRefs } from "pinia";
 import NotificationsManager from "@/components/NotificationsManager.vue";
 //store

+ 5 - 5
frontend/src/api.ts

@@ -1,6 +1,6 @@
 import axios from "axios";
 import { apiUrl } from "@/env";
-import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, IUserProfileRegister } from "@/interfaces";
+import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate} from "@/interfaces";
 
 function authHeaders(token: string) {
   return {
@@ -40,14 +40,14 @@ export const api = {
   async passwordRecovery(email: string) {
     return axios.post(`${apiUrl}/api/v1/password-recovery/${email}`);
   },
-  async resetPassword(password: string, token: string) {
+  async resetPassword(token: string, password: string) {
     return axios.post(`${apiUrl}/api/v1/reset-password/`, {
+      token: token,
       new_password: password,
-      token,
     });
   },
-  async registerUser(data: IUserProfileRegister) {
-    return axios.post(`${apiUrl}/api/v1/users/register`, data);
+  async registerUser(data: IUserProfileCreate) {
+    return axios.post(`${apiUrl}/api/v1/users/open`, data);
   },
   async testCeleryMsg(token: string, data:{msg: string}){
     return axios.post<{msg:string}>(`${apiUrl}/api/v1/utils/test-celery/msg`, data, authHeaders(token));

+ 0 - 0
frontend/src/views/LoadingView.vue → frontend/src/components/Loading.vue


+ 22 - 10
frontend/src/components/Navbar.vue

@@ -1,12 +1,23 @@
 <script setup lang="ts">
-import { reactive, ref, onMounted } from "vue";
-let items = reactive([{ title: "English" }, { title: "中文" }]);
+import { reactive } from "vue";
+import { useI18n } from "vue-i18n";
+
+const { t, locale } = useI18n();
+
+let lang = reactive([
+  { title: "English", text: "en" },
+  { title: "中文", text: "zh" },
+]);
 
 let menu = reactive([
-  // { title: '首頁', link: '/' },
-  { title: "登入", link: "/login" },
-  { title: "註冊", link: "/signup" },
+  { title: "login", link: "/login" },
+  { title: "register", link: "/signup" },
 ]);
+
+function setLang(lang: String) {
+  locale.value = `${lang}`;
+  localStorage.setItem("lang", `${lang}`);
+}
 </script>
 
 <template>
@@ -21,9 +32,9 @@ let menu = reactive([
     <v-spacer></v-spacer>
 
     <v-toolbar-items>
-      <v-btn v-for="item in menu" :to="item.link" color="gray">{{
-        item.title
-      }}</v-btn>
+      <v-btn v-for="item in menu" :to="item.link" color="gray">
+        {{ t(`${item.title}`) }}</v-btn
+      >
       <v-menu>
         <template v-slot:activator="{ props }">
           <v-btn color="gray" v-bind="props">
@@ -45,9 +56,10 @@ let menu = reactive([
         </template>
         <v-list>
           <v-list-item
-            v-for="(item, index) in items"
+            v-for="(item, index) in lang"
             :key="index"
-            :value="index"
+            :value="item.text"
+            @click="setLang(item.text)"
           >
             <v-list-item-title>{{ item.title }}</v-list-item-title>
           </v-list-item>

+ 1 - 1
frontend/src/components/NotificationsManager.vue

@@ -12,7 +12,7 @@
 
 <script setup lang="ts">
   import { ref, computed, watch } from 'vue';
-  import type { AppNotification } from '@/stores/main';
+  import type { AppNotification } from '@/interfaces';
   import { useMainStore } from '@/stores/main';
   import { storeToRefs } from 'pinia';
 

+ 15 - 4
frontend/src/interfaces/index.ts

@@ -22,7 +22,18 @@ export interface IUserProfileCreate {
     is_superuser?: boolean;
 }
 
-export interface IUserProfileRegister {
-    email: string;
-    password: string;
-}
+export interface AppNotification {
+  content: string;
+  color?: string;
+  showProgress?: boolean;
+};
+
+export interface MainState {
+  token: string;
+  isLoggedIn: boolean | null;
+  logInError: boolean;
+  userProfile: IUserProfile | null;
+  dashboardMiniDrawer: boolean;
+  dashboardShowDrawer: boolean;
+  notifications: AppNotification[];
+};

+ 32 - 0
frontend/src/language/en.json

@@ -0,0 +1,32 @@
+{
+    "login" : "Login",
+    "loginLink" : "Login",
+    "logout": "Logout",
+    "submit": "Submit",
+    "cancel": "Cancel",
+    "userName": "User name",
+    "password": "Password",
+    "emailAddress": "Email Address",
+    "registerPassword": "Password",
+    "confirmPassword": "Confirm Password",
+    "passwordLength": "(4-12 Characters long)",
+    "passwordConfirm": "(Type your password again)",
+    "haveAccount": "Have an account?",
+    "haventAccount": "Don't have an account?",
+    "register": "Register",
+    "registerLink": "Register",
+    "privacy_term_1": "Registeration implies acception of \n \n \n",
+    "privacy_term_2": "terms of service and privacy policy",
+    "forgotPsd": "Forgot Password",
+    "passwordRecovery": "Password Recovery",
+    "describe_1": "Make your first video for promotion, creation and life today",
+    "describe_2": "Let's get started with AI Presentors",
+    "dashboard": "Dashboard",
+    "makeVideo": "Make Video",
+    "progress": "Progress",
+    "userProfile": "User Profile",
+    "editProfile": "Edit Profile",
+    "changePassword": "Change Password",
+    "collapse": "Collapse",
+    "language": "Language"
+}

+ 32 - 0
frontend/src/language/zh.json

@@ -0,0 +1,32 @@
+{
+    "login" : "登入",
+    "loginLink" : "立即登入",
+    "logout": "登出",
+    "submit": "送出",
+    "cancel": "取消",
+    "userName": "使用者名稱",
+    "password": "密碼",
+    "emailAddress": "電子信箱",
+    "registerPassword": "設定密碼",
+    "confirmPassword": "確認密碼",
+    "passwordLength": "(4-12 位數密碼)",
+    "passwordConfirm": "(再次輸入您的密碼)",
+    "haveAccount": "已經有帳號?",
+    "haventAccount": "還沒有帳號?",
+    "register": "註冊",
+    "registerLink": "立即註冊",
+    "privacy_term_1": "註冊即表示您已閱讀並同意",
+    "privacy_term_2": "服務條款及隱私權政策",
+    "forgotPsd": "忘記密碼",
+    "passwordRecovery": "忘記您的密碼嗎?",
+    "describe_1": "將您的生活、創作、宣傳做成影片",
+    "describe_2": "開始使用 AI Presentors",
+    "dashboard": "首頁",
+    "makeVideo": "製作影片",
+    "progress": "影片清單",
+    "userProfile": "會員資料",
+    "editProfile": "編輯資料",
+    "changePassword": "變更密碼",
+    "collapse": "收合",
+    "language": "語言"
+}

+ 15 - 0
frontend/src/main.ts

@@ -3,11 +3,26 @@ import router from "./router";
 import App from "./App.vue";
 import { vuetify } from "./plugins/vuetify";
 import { pinia } from "./plugins/pinia";
+import { createI18n } from "vue-i18n";
+import zh from "./language/zh.json";
+import en from "./language/en.json";
+
+const i18n = createI18n({
+  legacy: false,
+  locale: localStorage.getItem("lang") ?? "zh",
+  fallbackLocale: "zh",
+  globalInjection: true,
+  messages: {
+    "zh": zh,
+    "en": en,
+  }
+});
 
 const app = createApp(App);
 
 app.use(pinia);
 app.use(router);
 app.use(vuetify);
+app.use(i18n);
 
 app.mount("#app");

+ 112 - 112
frontend/src/stores/admin.ts

@@ -9,121 +9,121 @@ interface AdminState {
 }
 
 const defaultState: AdminState = {
-    users: [],
+  users: [],
 };
   
 export const useAdminStore = defineStore("AdminStoreId", {
-    state: () => defaultState,
-    getters: {
-        readAdminUsers: (state) => state.users,
-        readAdminOneUser: (state) => {
-          return (userId:number) => state.users.filter((user) => user.id === userId)[0] || undefined;
-        },
-    },
-    actions: {
-        // setters
-        setUsers(payload: IUserProfile[]) {
-          this.users = payload;
-        },
-        setUser(payload: IUserProfile) {
-          const users = this.users.filter((user: IUserProfile) => user.id !== payload.id);
-          users.push(payload);
-          this.users = users;
-        },
-        
-        // actions
-        async actionGetUsers(){
-          const mainStore = useMainStore();
-          try {
-              const response = await api.getUsers(mainStore.token)
-              if (response) {
-                this.setUsers(response.data);
-              }
-          } catch (error) {
-              await mainStore.checkApiError(error);
-          }
-        },
-        async actionUpdateUser(payload: { id: number; user: IUserProfileUpdate }) {
-            const mainStore = useMainStore();
-            try {
-                const loadingNotification = { content: "saving", showProgress: true };
-                mainStore.addNotification(loadingNotification);
-                const response = (
-                  await Promise.all([
-                    api.updateUser(mainStore.token, payload.id, payload.user),
-                    await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
-                  ])
-                )[0];
-                this.setUser(response.data);
-                mainStore.removeNotification(loadingNotification);
-                mainStore.addNotification({
-                  content: "User successfully updated",
-                  color: "success",
-                });
-            } catch (error) {
-                await mainStore.checkApiError(error);
-            }
-        },
-        async actionCreateUser(payload: IUserProfileCreate) {
-            const mainStore = useMainStore();
-            try {
-              const loadingNotification = { content: "saving", showProgress: true };
-              mainStore.addNotification(loadingNotification);
-              const response = (
-                await Promise.all([
-                  api.createUser(mainStore.token, payload),
-                  await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
-                ])
-              )[0];
-              this.setUser(response.data);
-              mainStore.removeNotification(loadingNotification);
-              mainStore.addNotification({
-                content: "User successfully created",
-                color: "success",
-              });
-            } catch (error) {
-              await mainStore.checkApiError(error);
-            }
-        },
-        async actionTestCeleryMsg(payload: {msg:string}) {
-          const mainStore = useMainStore();
-          try {
-            const loadingNotification = { content: "sending", showProgress: true };
-            mainStore.addNotification(loadingNotification);
-            const response = (
-              await Promise.all([
-                api.testCeleryMsg(mainStore.token, payload),
-                await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
-              ])
-            );
-            mainStore.removeNotification(loadingNotification);
-            mainStore.addNotification({
-              content: "Word received",
-                color: "success",
-            })
-          } catch (error) {
-            await mainStore.checkApiError(error);
-          }
-        },
-        async actionTestCeleryFile(payload: File) {
-          const mainStore = useMainStore();
-          try {
-            const loadingNotification = { content: "sending", showProgress: true };
-            mainStore.addNotification(loadingNotification);
-            const response = (
-              await Promise.all([
-                api.testCeleryFile(mainStore.token, payload),
-                await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
-              ])
-            );
-            mainStore.removeNotification(loadingNotification);
-            mainStore.addNotification({
-              content: "File received",
-                color: "success",
-            })
-          } catch (error) {
-            await mainStore.checkApiError(error);
+  state: () => defaultState,
+  getters: {
+      readAdminUsers: (state) => state.users,
+      readAdminOneUser: (state) => {
+        return (userId:number) => state.users.filter((user) => user.id === userId)[0] || undefined;
+      },
+  },
+  actions: {
+      // setters
+      setUsers(payload: IUserProfile[]) {
+        this.users = payload;
+      },
+      setUser(payload: IUserProfile) {
+        const users = this.users.filter((user: IUserProfile) => user.id !== payload.id);
+        users.push(payload);
+        this.users = users;
+      },
+      
+      // actions
+      async actionGetUsers(){
+        const mainStore = useMainStore();
+        try {
+          const response = await api.getUsers(mainStore.token)
+          if (response) {
+            this.setUsers(response.data);
           }
+        } catch (error) {
+          await mainStore.checkApiError(error);
+        }
+      },
+      async actionUpdateUser(payload: { id: number; user: IUserProfileUpdate }) {
+        const mainStore = useMainStore();
+        try {
+          const loadingNotification = { content: "saving", showProgress: true };
+          mainStore.addNotification(loadingNotification);
+          const response = (
+            await Promise.all([
+              api.updateUser(mainStore.token, payload.id, payload.user),
+              await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
+            ])
+          )[0];
+          this.setUser(response.data);
+          mainStore.removeNotification(loadingNotification);
+          mainStore.addNotification({
+            content: "User successfully updated",
+            color: "success",
+          });
+        } catch (error) {
+          await mainStore.checkApiError(error);
+        }
+      },
+      async actionCreateUser(payload: IUserProfileCreate) {
+        const mainStore = useMainStore();
+        try {
+          const loadingNotification = { content: "saving", showProgress: true };
+          mainStore.addNotification(loadingNotification);
+          const response = (
+            await Promise.all([
+              api.createUser(mainStore.token, payload),
+              await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
+            ])
+          )[0];
+          this.setUser(response.data);
+          mainStore.removeNotification(loadingNotification);
+          mainStore.addNotification({
+            content: "User successfully created",
+            color: "success",
+          });
+        } catch (error) {
+          await mainStore.checkApiError(error);
+        }
+      },
+      async actionTestCeleryMsg(payload: {msg:string}) {
+        const mainStore = useMainStore();
+        try {
+          const loadingNotification = { content: "sending", showProgress: true };
+          mainStore.addNotification(loadingNotification);
+          const response = (
+            await Promise.all([
+              api.testCeleryMsg(mainStore.token, payload),
+              await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
+            ])
+          );
+          mainStore.removeNotification(loadingNotification);
+          mainStore.addNotification({
+            content: "Word received",
+              color: "success",
+          })
+        } catch (error) {
+          await mainStore.checkApiError(error);
+        }
+      },
+      async actionTestCeleryFile(payload: File) {
+        const mainStore = useMainStore();
+        try {
+          const loadingNotification = { content: "sending", showProgress: true };
+          mainStore.addNotification(loadingNotification);
+          const response = (
+            await Promise.all([
+              api.testCeleryFile(mainStore.token, payload),
+              await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
+            ])
+          );
+          mainStore.removeNotification(loadingNotification);
+          mainStore.addNotification({
+            content: "File received",
+              color: "success",
+          })
+        } catch (error) {
+          await mainStore.checkApiError(error);
         }
-    },
+      }
+  },
 });

+ 216 - 231
frontend/src/stores/main.ts

@@ -3,244 +3,229 @@ import { defineStore } from "pinia";
 import { api } from "@/api"
 import router from "@/router"
 import { getLocalToken, removeLocalToken, saveLocalToken } from "@/utils";
-import type { IUserProfile, IUserProfileRegister, IUserProfileUpdate } from '@/interfaces';
-
-export interface AppNotification {
-  content: string;
-  color?: string;
-  showProgress?: boolean;
-};
-
-interface MainState {
-  token: string;
-  isLoggedIn: boolean | null;
-  logInError: boolean;
-  userProfile: IUserProfile | null;
-  dashboardMiniDrawer: boolean;
-  dashboardShowDrawer: boolean;
-  notifications: AppNotification[];
-};
+import type { AppNotification } from '@/interfaces';
+import type { IUserProfile, IUserProfileCreate, IUserProfileUpdate, MainState } from '@/interfaces';
 
 const defaultState: MainState = {
-    isLoggedIn: null,
-    token: '',
-    logInError: false,
-    userProfile: null,
-    dashboardMiniDrawer: false,
-    dashboardShowDrawer: true,
-    notifications: [],
+  isLoggedIn: null,
+  token: '',
+  logInError: false,
+  userProfile: null,
+  dashboardMiniDrawer: false,
+  dashboardShowDrawer: true,
+  notifications: [],
 };
 
 export const useMainStore = defineStore("MainStoreId", {
-    state: () => defaultState,
-    
-    getters: {
-        readhasAdminAccess: (state) => 
-            state.userProfile && state.userProfile.is_superuser && state.userProfile.is_active,
-        readLoginError: (state) => state.logInError,
-        readDashboardShowDrawer: (state) => state.dashboardShowDrawer,
-        readDashboardMiniDrawer: (state) => state.dashboardMiniDrawer,
-        readUserProfile: (state) => state.userProfile,
-        readToken: (state) => state.token,
-        readIsLoggedIn: (state) => state.isLoggedIn,
-        readFirstNotification: (state) => state.notifications.length > 0 && state.notifications[0], 
+  state: () => defaultState,
+  
+  getters: {
+    readhasAdminAccess: (state) => 
+        state.userProfile && state.userProfile.is_superuser && state.userProfile.is_active,
+    readLoginError: (state) => state.logInError,
+    readDashboardShowDrawer: (state) => state.dashboardShowDrawer,
+    readDashboardMiniDrawer: (state) => state.dashboardMiniDrawer,
+    readUserProfile: (state) => state.userProfile,
+    readToken: (state) => state.token,
+    readIsLoggedIn: (state) => state.isLoggedIn,
+    readFirstNotification: (state) => state.notifications.length > 0 && state.notifications[0], 
+  },
+  
+  actions:{
+    // setters
+    setToken(payload: string) { this.token = payload; },
+    setLoggedIn(payload: boolean) { this.isLoggedIn = payload; },
+    setLogInError(payload: boolean) { this.logInError = payload; },
+    setUserProfile(payload: IUserProfile) { this.userProfile = payload },
+    setDashboardMiniDrawer(payload: boolean) { this.dashboardMiniDrawer = payload; },
+    setDashboardShowDrawer(payload: boolean) { this.dashboardShowDrawer = payload; },
+    addNotification(payload: AppNotification) { this.notifications.push(payload); },
+    removeNotification(payload: AppNotification) {
+      if (payload) { 
+        this.notifications = this.notifications.filter(
+          (notification) => notification !== payload,
+        );
+      }
+    },
+    // actions
+    async logIn(username:string, password:string) {
+      try {
+        const response = await api.logInGetToken(username, password);
+        const token: string = response.data.access_token;
+        if (token) {
+          saveLocalToken(token);
+          this.setToken(token);
+          this.setLoggedIn(true);
+          this.setLogInError(false);
+          await this.getUserProfile();
+          await this.routeLoggedIn();
+          this.addNotification({content: "Logged in", color: "success" });
+        } else {
+          await this.logOut();
+        }
+      } catch (err) {
+        this.setLogInError(true);
+        await this.logOut();
+      }
     },
-    
-    actions:{
-        // setters
-        setToken(payload: string) { this.token = payload; },
-        setLoggedIn(payload: boolean) { this.isLoggedIn = payload; },
-        setLogInError(payload: boolean) { this.logInError = payload; },
-        setUserProfile(payload: IUserProfile) { this.userProfile = payload },
-        setDashboardMiniDrawer(payload: boolean) { this.dashboardMiniDrawer = payload; },
-        setDashboardShowDrawer(payload: boolean) { this.dashboardShowDrawer = payload; },
-        addNotification(payload: AppNotification) { this.notifications.push(payload); },
-        removeNotification(payload: AppNotification) {
-            if (payload) { 
-                this.notifications = this.notifications.filter(
-                    (notification) => notification !== payload,
-                );
-            }
-        },
-        // actions
-        async logIn(username:string, password:string) {
-            try {
-                const response = await api.logInGetToken(username, password);
-                const token: string = response.data.access_token;
-                if (token) {
-                    saveLocalToken(token);
-                    this.setToken(token);
-                    this.setLoggedIn(true);
-                    this.setLogInError(false);
-                    await this.getUserProfile();
-                    await this.routeLoggedIn();
-                    this.addNotification({content: "Logged in", color: "success" });
-                } else {
-                    await this.logOut();
-                }
-            } catch (err) {
-                this.setLogInError(true);
-                await this.logOut();
-            }
-        },
-        async getUserProfile() {
-            try {
-                const response = await api.getMe(this.token);
-                if (response.data) {
-                    this.setUserProfile(response.data)
-                }
-            } catch (error) {
-                await this.checkApiError(error);
-            }
-        },  
-        async updateUserProfile(payload: IUserProfileUpdate) {
-            try {
-                const loadingNotification = { content: "saving", showProgress: true };
-                await this.addNotification(loadingNotification);
-                const response = (
-                  await Promise.all([
-                    api.updateMe(this.token, payload),
-                    await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
-                  ])
-                )[0];
-                this.setUserProfile(response.data);
-                this.removeNotification(loadingNotification);
-                this.addNotification({
-                  content: "Profile successfully updated",
-                  color: "success",
-                });
-            } catch (error) {
-                await this.checkApiError(error);
-            }
-        },
-        async checkLoggedIn() {
-            if (!this.isLoggedIn) {
-                let token = this.token;
-                if (!token) {
-                  const localToken = getLocalToken();
-                  if (localToken) {
-                    this.setToken(localToken);
-                    token = localToken;
-                  }
-                }
-                if (token) {
-                  try {
-                    const response = await api.getMe(token);
-                    this.setLoggedIn(true);
-                    this.setUserProfile(response.data);
-                  } catch (error) {
-                    await this.removeLogIn();
-                  }
-                } else {
-                  await this.removeLogIn();
-                }
-            }
-        },
-        async removeLogIn() {
-            removeLocalToken();
-            this.setToken("");
-            this.setLoggedIn(false);
-        },
-        async logOut() {
+    async getUserProfile() {
+      try {
+        const response = await api.getMe(this.token);
+        if (response.data) {
+          this.setUserProfile(response.data)
+        }
+      } catch (error) {
+        await this.checkApiError(error);
+      }
+    },  
+    async updateUserProfile(user: IUserProfileUpdate) {
+      try {
+        const loadingNotification = { content: "saving", showProgress: true };
+        await this.addNotification(loadingNotification);
+        const response = (
+          await Promise.all([
+            api.updateMe(this.token, user),
+            await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
+          ])
+        )[0];
+        this.setUserProfile(response.data);
+        this.removeNotification(loadingNotification);
+        this.addNotification({
+          content: "Profile successfully updated",
+          color: "success",
+        });
+      } catch (error) {
+        await this.checkApiError(error);
+      }
+    },
+    async checkLoggedIn() {
+      if (!this.isLoggedIn) {
+        let token = this.token;
+        if (!token) {
+          const localToken = getLocalToken();
+          if (localToken) {
+            this.setToken(localToken);
+            token = localToken;
+          }
+        }
+        if (token) {
+          try {
+            const response = await api.getMe(token);
+            this.setLoggedIn(true);
+            this.setUserProfile(response.data);
+          } catch (error) {
             await this.removeLogIn();
-            this.routeLogOut();
-        },
-        async userLogOut() {
-            await this.logOut();
-            this.addNotification({ content: "Logged out", color: "success" });
-        },
-        routeLogOut() {
-            if (router.currentRoute.value.path !== "/login") {
-                router.push("/login");
-            }
-        },
-        async checkApiError(payload: unknown) {
-            if (axios.isAxiosError(payload)) {
-                if (payload.response?.status === 401) {
-                  await this.logOut();
-                }
-            }
-        },
-        routeLoggedIn() {
-            if (router.currentRoute.value.path === "/login" || router.currentRoute.value.path === "/") {
-                router.push("/main/dashboard");
-            }
-        },
-        async dispatchRemoveNotification(payload: {notification: AppNotification; timeout: number },) {
-            return new Promise((resolve, _) => {
-                setTimeout(() => {
-                  this.removeNotification(payload.notification);
-                  resolve(true);
-                }, payload.timeout);
-            });
-        },
-        async register(payload: IUserProfileRegister) {
-            const loadingNotification = {
-                content: "Sgining up...",
-                showProgress: true,
-            };
-            try {
-                this.addNotification(loadingNotification);
-                const response = (
-                  await Promise.all([
-                    api.registerUser(payload),
-                    await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
-                  ])
-                )[0];
-                this.removeNotification(loadingNotification);
-                this.addNotification({
-                  content: "successfully registered",
-                  color: "success",
-                });
-            } catch (error) {
-              await this.checkApiError(error);
           }
-        },
-        async passwordRecovery(payload: {username: string }) {
-            const loadingNotification = {
-                content: "Sending password recovery email",
-                showProgress: true,
-            };
-            try {
-                this.addNotification(loadingNotification);
-                await Promise.all([
-                  api.passwordRecovery(payload.username),
-                  await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
-                ]);
-                this.removeNotification(loadingNotification);
-                this.addNotification({
-                  content: "Password recovery email sent",
-                  color: "success",
-                });
-                await this.logOut();
-            } catch (error) {
-                this.removeNotification(loadingNotification);
-                this.addNotification({ color: "error", content: "Incorrect username" });
-            }
-        },
-        async resetPassword(payload: {password: string; token: string }) {
-            const loadingNotification = { content: "Resetting password", showProgress: true };
-            try {
-                this.addNotification(loadingNotification);
-                await Promise.all([
-                    api.resetPassword(payload.password, payload.token),
-                    await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
-                ]);
-                this.removeNotification(loadingNotification);
-                this.addNotification( {
-                    content: "Password successfully reset",
-                    color: "success",
-                });
-                await this.logOut();
-            } catch (error) {
-                this.removeNotification(loadingNotification);
-                this.addNotification({
-                    color: "error",
-                    content: "Error resetting password",
-                });
-            }
-        },
-    }
-  });
+        } else {
+          await this.removeLogIn();
+        }
+      }
+    },
+    async removeLogIn() {
+      removeLocalToken();
+      this.setToken("");
+      this.setLoggedIn(false);
+    },
+    async logOut() {
+      await this.removeLogIn();
+      this.routeLogOut();
+  },
+    async userLogOut() {
+      await this.logOut();
+      this.addNotification({ content: "Logged out", color: "success" });
+    },
+    routeLogOut() {
+      if (router.currentRoute.value.path !== "/login") {
+          router.push("/login");
+      }
+    },
+    async checkApiError(payload: unknown) {
+      if (axios.isAxiosError(payload)) {
+        if (payload.response?.status === 401) {
+          await this.logOut();
+        }
+      }
+    },
+    routeLoggedIn() {
+      if (router.currentRoute.value.path === "/login" || router.currentRoute.value.path === "/") {
+        router.push("/main/dashboard");
+      }
+    },
+    async dispatchRemoveNotification(payload: {notification: AppNotification; timeout: number },) {
+      return new Promise((resolve, _) => {
+        setTimeout(() => {
+          this.removeNotification(payload.notification);
+          resolve(true);
+        }, payload.timeout);
+      });
+    },
+    async register(user: IUserProfileCreate) {
+      const loadingNotification = {
+        content: "Sgining up...",
+        showProgress: true,
+      };
+      try {
+        this.addNotification(loadingNotification);
+        const response = (
+          await Promise.all([
+            api.registerUser(user),
+            await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
+          ])
+        )[0];
+        this.removeNotification(loadingNotification);
+        this.addNotification({
+          content: "successfully registered",
+          color: "success",
+        });
+      } catch (error) {
+        await this.checkApiError(error);
+      }
+    },
+    async passwordRecovery(email: string) {
+      const loadingNotification = {
+        content: "Sending password recovery email",
+        showProgress: true,
+      };
+      try {
+        this.addNotification(loadingNotification);
+        await Promise.all([
+          api.passwordRecovery(email),
+          await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
+        ]);
+        this.removeNotification(loadingNotification);
+        this.addNotification({
+          content: "Password recovery email sent",
+          color: "success",
+        });
+        await this.logOut();
+      } catch (error) {
+        this.removeNotification(loadingNotification);
+        this.addNotification({ color: "error", content: "Incorrect username" });
+      }
+    },
+    async resetPassword(token: string, password: string ) {
+      const loadingNotification = { content: "Resetting password", showProgress: true };
+      try {
+        this.addNotification(loadingNotification);
+        await Promise.all([
+          api.resetPassword(token, password),
+          await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 500)),
+        ]);
+        this.removeNotification(loadingNotification);
+        this.addNotification( {
+          content: "Password successfully reset",
+          color: "success",
+        });
+        await this.logOut();
+      } catch (error) {
+        this.removeNotification(loadingNotification);
+        this.addNotification({
+          color: "error",
+          content: "Error resetting password",
+        });
+      }
+    },
+  }
+});
 
 

+ 19 - 6
frontend/src/views/Login.vue

@@ -4,6 +4,7 @@ import { ref, computed, onMounted } from "vue";
 import { useMainStore } from "@/stores/main";
 import { useDisplay } from "vuetify";
 import { storeToRefs } from "pinia";
+import { useI18n } from "vue-i18n";
 import Navbar from "@/components/Navbar.vue";
 
 const mainStore = useMainStore();
@@ -13,6 +14,7 @@ const mainStoreRef = storeToRefs(mainStore);
 const email = ref("");
 const password = ref("");
 const { name } = useDisplay();
+const { t } = useI18n();
 let showPassword = ref(false);
 
 // getter
@@ -48,15 +50,15 @@ onMounted(() => {});
         <section class="overflow-hidden banner-item">
           <img src="../assets/img/banner.png" alt="" />
           <h2>
-            將您的生活、創作、宣傳做成影片
+            {{ t("describe_1") }}
             <br />
-            開始使用 AI Presentors
+            {{ t("describe_2") }}
           </h2>
         </section>
       </v-col>
       <v-col :cols="width" class="px-6 my-8 my-md-0">
         <div class="form-title">
-          <h3>登入</h3>
+          <h3>{{ t("login") }}</h3>
           <span></span>
         </div>
         <v-form ref="form" class="login-form" lazy-validation>
@@ -65,7 +67,7 @@ onMounted(() => {});
             name="email"
             prepend-icon="person"
             :rules="[(v) => !!v || '請輸入您的帳號']"
-            label="使用者名稱"
+            :label="$t('emailAddress')"
             required
           ></v-text-field>
 
@@ -77,18 +79,26 @@ onMounted(() => {});
             :append-icon="showPassword ? 'visibility' : 'visibility_off'"
             :rules="[(v) => !!v || '請輸入您的密碼']"
             :type="showPassword ? 'text' : 'password'"
-            label="密碼"
+            :label="$t('password')"
             hint="4-12 位數密碼"
             @click:append="showPassword = !showPassword"
             required
           ></v-text-field>
 
           <p class="text-center">
+<<<<<<< HEAD
             還沒有帳號?
             <router-link to="/signup">註冊</router-link> / <router-link
               to="/recover-password"
               >忘記密碼</router-link
             >
+=======
+            {{ t("haventAccount") }}
+            <router-link to="/signup">{{ t("register") }}</router-link
+            >/<router-link to="/recover-password">{{
+              t("forgotPsd")
+            }}</router-link>
+>>>>>>> choozmo/front-dev
           </p>
 
           <v-btn
@@ -97,11 +107,14 @@ onMounted(() => {});
             @click.prevent="submit"
             class="login-btn"
           >
-            立即登入
+            {{ t("loginLink") }}
           </v-btn>
         </v-form>
       </v-col>
     </v-row>
   </v-container>
+<<<<<<< HEAD
 
+=======
+>>>>>>> choozmo/front-dev
 </template>

+ 61 - 47
frontend/src/views/PasswordRecovery.vue

@@ -1,61 +1,75 @@
-<template>
-    <v-container fluid class="d-flex fill-height">
-      <v-row align="center" justify="center">
-        <v-col :cols="width">
-          <v-card class="elevation-12">
-            <v-toolbar dark color="primary">
-              <v-toolbar-title>{{appName}} - Password Recovery</v-toolbar-title>
-            </v-toolbar>
-            <v-card-text>
-              <p class="subheading">A password recovery email will be sent to the registered account</p>
-              <v-form @keyup.enter="submit" v-model="valid" ref="form" @submit.prevent="" lazy-validation>
-                <v-text-field @keyup.enter="submit" label="Username" type="text" prepend-icon="person" v-model="username" :rules="nameRules" required></v-text-field>
-              </v-form>
-            </v-card-text>
-            <v-card-actions>
-              <v-spacer></v-spacer>
-              <v-btn @click="cancel">Cancel</v-btn>
-              <v-btn @click.prevent="submit" :disabled="!valid">
-                Recover Password
-              </v-btn>
-            </v-card-actions>
-          </v-card>
-        </v-col>
-      </v-row>
-    </v-container>
-</template>
-
 <script setup lang="ts">
-import { appName } from '@/env';
-import { useMainStore } from '@/stores/main';
-import { ref, computed } from 'vue';
-import { nameRules } from '@/utils';
-import  router  from '@/router'
-import { useDisplay } from 'vuetify';
+import router from "@/router";
+import { useMainStore } from "@/stores/main";
+import { ref, computed } from "vue";
+import { required } from "@/utils";
+import { useDisplay } from "vuetify";
+import { useI18n } from "vue-i18n";
+import Navbar from "@/components/Navbar.vue";
 
+let email = ref("");
 const valid = ref(true);
-const username = ref('');
 
+const { t } = useI18n();
 const mainStore = useMainStore();
-
 const { name } = useDisplay();
 const width = computed(() => {
-        // name is reactive and
-        // must use .value
-        switch (name.value) {
-          case 'xs': return 12
-          case 'sm': return 8
-          case 'md': return 4
-        }
-
-        return 4
-      })
+  switch (name.value) {
+    case "xs":
+      return 12;
+  }
+  return 8;
+});
 
 function cancel() {
   router.back();
 }
 
 function submit() {
-  mainStore.passwordRecovery({username: username.value});
+  mainStore.passwordRecovery(email.value);
 }
-</script>
+</script>
+
+<template>
+  <Navbar />
+  <v-container class="d-flex fill-height">
+    <v-row align="center" justify="center">
+      <v-col :cols="width">
+        <v-card class="elevation-12">
+          <v-toolbar dark color="primary">
+            <v-toolbar-title>{{ t("passwordRecovery") }}</v-toolbar-title>
+          </v-toolbar>
+          <v-card-text>
+            <p class="subheading mb-3">
+              A password recovery email will be sent to the registered account
+            </p>
+            <v-form
+              @keyup.enter="submit"
+              v-model="valid"
+              ref="form"
+              @submit.prevent=""
+              lazy-validation
+            >
+              <v-text-field
+                @keyup.enter="submit"
+                :label="$t('emailAddress')"
+                type="text"
+                prepend-icon="email"
+                v-model="email"
+                :rules="required"
+                required
+              ></v-text-field>
+            </v-form>
+          </v-card-text>
+          <v-card-actions>
+            <v-spacer></v-spacer>
+            <v-btn @click="cancel">{{ t("cancel") }}</v-btn>
+            <v-btn @click.prevent="submit" :disabled="!valid">
+              {{ t("submit") }}
+            </v-btn>
+          </v-card-actions>
+        </v-card>
+      </v-col>
+    </v-row>
+  </v-container>
+</template>

+ 1 - 1
frontend/src/views/ResetPassword.vue

@@ -86,7 +86,7 @@ async function submit(){
   if (valid.value) {
     const token = checkToken();
     if (token) {
-      await mainStore.resetPassword({token, password: password1.value});
+      await mainStore.resetPassword(token, password1.value);
       router.push('/');
     }
   }

+ 72 - 102
frontend/src/views/Signup.vue

@@ -4,6 +4,7 @@ import { ref, reactive, computed, onMounted } from "vue";
 import { useMainStore } from "@/stores/main";
 import { useDisplay } from "vuetify";
 import { storeToRefs } from "pinia";
+import { useI18n } from "vue-i18n";
 import Navbar from "@/components/Navbar.vue";
 
 const mainStore = useMainStore();
@@ -13,10 +14,12 @@ const mainStoreRef = storeToRefs(mainStore);
 const email = ref("");
 const password = ref("");
 const { name } = useDisplay();
+const { t } = useI18n();
 const confirmPassword = ref("");
 let data = reactive({
   email: "",
   password: "",
+  full_name: ""
 });
 let dialog = ref(false);
 let confirmState = ref(false);
@@ -40,7 +43,6 @@ const width = computed(() => {
 // action
 async function submit() {
   if (confirmPassword.value === data.password) {
-    console.log("data", data.password);
     mainStore.register(data);
   } else {
     confirmState.value = true;
@@ -60,29 +62,29 @@ async function submit() {
         <section class="overflow-hidden banner-item">
           <img src="../assets/img/banner.png" alt="" />
           <h2>
-            將您的生活、創作、宣傳做成影片
+            {{ t("describe_1") }}
             <br />
-            開始使用 AI Presentors
+            {{ t("describe_2") }}
           </h2>
         </section>
       </v-col>
       <v-col :cols="width" class="px-6 my-8 my-md-0">
         <div class="form-title">
-          <h3>註冊</h3>
+          <h3>{{ t("register") }}</h3>
           <span></span>
         </div>
         <v-form ref="form" class="login-form" lazy-validation>
-          <!-- <v-text-field
+          <v-text-field
             v-model="data.full_name"
             :rules="[(v) => !!v || '請輸入您的帳號']"
             label="使用者名稱"
             required
-          ></v-text-field> -->
+          ></v-text-field>
 
           <v-text-field
             v-model="data.email"
             :rules="[(v) => !!v || '請輸入您的電子信箱']"
-            label="電子信箱"
+            :label="$t('emailAddress')"
             required
           ></v-text-field>
 
@@ -91,8 +93,8 @@ async function submit() {
             :append-icon="showPassword ? 'visibility' : 'visibility_off'"
             :rules="[(v) => !!v || '請輸入您的密碼']"
             :type="showPassword ? 'text' : 'password'"
-            label="設定密碼"
-            hint="4-12 位數密碼"
+            :label="$t('registerPassword')"
+            :hint="$t('passwordLength')"
             @click:append="showPassword = !showPassword"
             required
           ></v-text-field>
@@ -102,8 +104,8 @@ async function submit() {
             :append-icon="showConfirmPassword ? 'visibility' : 'visibility_off'"
             :rules="[(v) => !!v || '請輸入您的密碼']"
             :type="showConfirmPassword ? 'text' : 'password'"
-            label="確認密碼"
-            hint="再次輸入您的密碼"
+            :label="$t('confirmPassword')"
+            :hint="$t('passwordConfirm')"
             @click:append="showConfirmPassword = !showConfirmPassword"
             required
           ></v-text-field>
@@ -113,7 +115,8 @@ async function submit() {
           </v-alert>
 
           <p class="text-center">
-            已經有帳號? <router-link to="/login">登入</router-link>
+            {{ t("haveAccount") }}
+            <router-link to="/login"> {{ t("login") }}</router-link>
           </p>
 
           <v-btn
@@ -122,105 +125,72 @@ async function submit() {
             @click.prevent="submit"
             class="login-btn"
           >
-            立即註冊
+            {{ t("registerLink") }}
           </v-btn>
 
           <section
             class="mt-5 d-flex align-center justify-center dialog-content"
           >
-            <p>註冊即表示您已閱讀並同意</p>
-            <v-dialog v-model="dialog" max-width="700" scrollable>
-              <template v-slot:activator="{ props }">
-                <v-btn
-                  variant="text"
-                  color="primary"
-                  v-bind="props"
-                  class="px-1"
-                >
-                  服務條款及隱私權政策
-                </v-btn>
-              </template>
-
-              <v-card class="terms-card">
-                <v-card-title>
-                  <v-spacer></v-spacer>
-                  <v-btn icon @click="dialog = false">
-                    <v-icon icon="md:close"></v-icon>
-                  </v-btn>
-                </v-card-title>
-                <v-card-text>
-                  <h3 class="text-h5 text-center font-weight-bold mb-5">
-                    使用者的守法義務
-                  </h3>
-                  您承諾絕不為任何非法目的或以任何非法方式使用本服務,並承諾遵守中華民國相關法規及一切使用網際網路之國際慣例。您若係中華民國以外之使用者,並同意遵守所屬國家或地域之法令。您同意並保證不得利用本服務從事侵害他人權益或違法之行為,包括但不限於:
-                  <ul class="mb-3">
-                    <li>
-                      上載、張貼、公布或傳送任何誹謗、侮辱、具威脅性、攻擊性、不雅、猥褻、不實、違反公共秩序或善良風俗或其他不法之文字、圖片或任何形式的檔案於本服務上
-                    </li>
-                    <li>
-                      侵害他人名譽、隱私權、營業秘密、商標權、著作權、專利權、其他智慧財產權及其他權利
-                    </li>
-                    <li>違反依法律或契約所應負之保密義務</li>
-                    <li>冒用他人名義使用本服務</li>
-                  </ul>
-                  <v-divider></v-divider>
-                  <h3 class="text-h5 text-center font-weight-bold mt-7 mb-5">
-                    免責聲明
-                  </h3>
-                  您明確了解並同意:ChoozMo
-                  對本服務不提供任何明示或默示的擔保,包含但不限於權利完整、商業適售性、特定目的之適用性及未侵害他人權利。本服務乃依其「現狀」及「提供使用時」之基礎提供,您使用本服務時,須自行承擔相關風險。ChoozMo
-                  不保證以下事項:
-                  <ul>
-                    <li>本服務將符合您的需求</li>
-                    <li>本服務不受干擾、及時提供、安全可靠或無錯誤</li>
-                    <li>由本服務之使用而取得之結果為正確或可靠</li>
-                  </ul>
-
-                  是否經由本服務之使用下載或取得任何資料應由您自行考量且自負風險,並拋棄因前開任何資料之下載而導致您電腦系統、網路存取、下載或播放設備之任何損壞或資料流失,對
-                  ChoozMo 提出任何請求或採取法律行動,您應自負完全責任。
-                </v-card-text>
-              </v-card>
-            </v-dialog>
+            <p>
+              {{ t("privacy_term_1") }}
+              <v-dialog v-model="dialog" max-width="700" scrollable>
+                <template v-slot:activator="{ props }">
+                  <a
+                    href="javascript:;"
+                    color="primary"
+                    v-bind="props"
+                    class="ms-1"
+                  >
+                    {{ t("privacy_term_2") }}
+                  </a>
+                </template>
+
+                <v-card class="terms-card">
+                  <v-card-title>
+                    <v-spacer></v-spacer>
+                    <v-btn icon @click="dialog = false">
+                      <v-icon icon="md:close"></v-icon>
+                    </v-btn>
+                  </v-card-title>
+                  <v-card-text>
+                    <h3 class="text-h5 text-center font-weight-bold mb-5">
+                      使用者的守法義務
+                    </h3>
+                    您承諾絕不為任何非法目的或以任何非法方式使用本服務,並承諾遵守中華民國相關法規及一切使用網際網路之國際慣例。您若係中華民國以外之使用者,並同意遵守所屬國家或地域之法令。您同意並保證不得利用本服務從事侵害他人權益或違法之行為,包括但不限於:
+                    <ul class="mb-3">
+                      <li>
+                        上載、張貼、公布或傳送任何誹謗、侮辱、具威脅性、攻擊性、不雅、猥褻、不實、違反公共秩序或善良風俗或其他不法之文字、圖片或任何形式的檔案於本服務上
+                      </li>
+                      <li>
+                        侵害他人名譽、隱私權、營業秘密、商標權、著作權、專利權、其他智慧財產權及其他權利
+                      </li>
+                      <li>違反依法律或契約所應負之保密義務</li>
+                      <li>冒用他人名義使用本服務</li>
+                    </ul>
+                    <v-divider></v-divider>
+                    <h3 class="text-h5 text-center font-weight-bold mt-7 mb-5">
+                      免責聲明
+                    </h3>
+                    您明確了解並同意:ChoozMo
+                    對本服務不提供任何明示或默示的擔保,包含但不限於權利完整、商業適售性、特定目的之適用性及未侵害他人權利。本服務乃依其「現狀」及「提供使用時」之基礎提供,您使用本服務時,須自行承擔相關風險。ChoozMo
+                    不保證以下事項:
+                    <ul>
+                      <li>本服務將符合您的需求</li>
+                      <li>本服務不受干擾、及時提供、安全可靠或無錯誤</li>
+                      <li>由本服務之使用而取得之結果為正確或可靠</li>
+                    </ul>
+
+                    是否經由本服務之使用下載或取得任何資料應由您自行考量且自負風險,並拋棄因前開任何資料之下載而導致您電腦系統、網路存取、下載或播放設備之任何損壞或資料流失,對
+                    ChoozMo 提出任何請求或採取法律行動,您應自負完全責任。
+                  </v-card-text>
+                </v-card>
+              </v-dialog>
+            </p>
           </section>
         </v-form>
       </v-col>
     </v-row>
   </v-container>
-
-  <!-- <v-container fluid class="d-flex fill-height">
-    <v-row align="center" justify="center">
-      <v-col :cols="width">
-        <v-card class="elevation-12">
-          <v-toolbar dark color="primary">
-            <v-toolbar-title>{{ appName }}</v-toolbar-title>
-            <v-spacer></v-spacer>
-            <v-btn to="/login">LogIn</v-btn>
-          </v-toolbar>
-          <v-card-text>
-            <v-form @keyup.enter="submit">
-              <v-text-field
-                @keyup.enter="submit"
-                v-model="email"
-                prepend-icon="person"
-                name="email"
-                label="Email"
-                type="text"
-              ></v-text-field>
-              <v-text-field
-                @keyup.enter="submit"
-                v-model="password"
-                prepend-icon="key"
-                name="password"
-                label="Password"
-                id="password"
-                type="password"
-              ></v-text-field>
-            </v-form>
-          </v-card-text>
-        </v-card>
-      </v-col>
-    </v-row>
-  </v-container> -->
 </template>
 
 <style lang="scss">
@@ -228,7 +198,7 @@ async function submit() {
   display: flex;
   align-items: center;
   justify-content: center;
-  font-size: 14px;
+  font-size: 13px;
   letter-spacing: 1px;
   .v-btn {
     &:hover > .v-btn__overlay {

+ 25 - 19
frontend/src/views/main/Dashboard.vue

@@ -1,3 +1,23 @@
+<script setup lang="ts">
+import { computed } from 'vue';
+import { useMainStore } from '@/stores/main';
+import { storeToRefs } from 'pinia';
+
+const mainStore = useMainStore();
+const mainStoreRef = storeToRefs(mainStore);
+
+const greetedUser = computed(() => {
+  const userProfile = mainStoreRef.readUserProfile;
+  if (userProfile.value) {
+    if (userProfile.value!.full_name){
+      return userProfile.value!.full_name;
+    } else {
+      return userProfile.value.email;
+    }
+  }
+});
+</script>
+
 <template>
   <v-container fluid>
     <v-card class="ma-3 pa-3">
@@ -16,22 +36,8 @@
   </v-container>
 </template>
 
-<script setup lang="ts">
-  import { computed } from 'vue';
-  import { useMainStore } from '@/stores/main';
-  import { storeToRefs } from 'pinia';
-
-  const mainStore = useMainStore();
-  const mainStoreRef = storeToRefs(mainStore);
-
-  const greetedUser = computed(() => {
-    const userProfile = mainStoreRef.readUserProfile;
-    if (userProfile.value) {
-      if (userProfile.value!.full_name){
-        return userProfile.value!.full_name;
-      } else {
-        return userProfile.value.email;
-      }
-    }
-  });
-</script>
+<style lang="scss" scoped>
+.v-toolbar__content {
+  background-image: linear-gradient(-225deg, rgb(234, 84, 19) 35%, rgb(178, 69, 146) 100%);
+}
+</style>

+ 146 - 93
frontend/src/views/main/Main.vue

@@ -1,41 +1,118 @@
+<script setup lang="ts">
+import { appName } from "@/env";
+import { reactive } from "vue";
+import type { RouteLocationNormalized, NavigationGuardNext } from "vue-router";
+import { onBeforeRouteUpdate } from "vue-router";
+import { useMainStore } from "@/stores/main";
+import { storeToRefs } from "pinia";
+import { useI18n } from "vue-i18n";
+
+
+
+    const { t, locale } = useI18n();
+    const mainStore = useMainStore();
+    const mainStoreRef = storeToRefs(mainStore);
+
+    const hasAdminAccess = mainStoreRef.readhasAdminAccess;
+
+    const miniDrawer = mainStoreRef.readDashboardMiniDrawer;
+    const showDrawer = mainStoreRef.readDashboardShowDrawer;
+
+    function switchMiniDrawer() {
+      mainStore.setDashboardMiniDrawer(
+        !mainStoreRef.readDashboardMiniDrawer.value
+      );
+    }
+
+    function switchShowDrawer() {
+      mainStore.setDashboardShowDrawer(
+        !mainStoreRef.readDashboardShowDrawer.value
+      );
+    }
+
+    function logout() {
+      mainStore.logOut();
+    }
+
+    onBeforeRouteUpdate((to, from, next) => {
+      routeGuardMain(to, from, next);
+    });
+
+    const lang = reactive([
+  { title: "English", text: "en" },
+  { title: "中文", text: "zh" },
+]);
+
+function setLang(lang: String) {
+  locale.value = `${lang}`;
+  localStorage.setItem("lang", `${lang}`);
+}
+
+    
+  
+  // beforeRouteEnter((to, from, next) =>{
+  //   routeGuardMain(to, from, next);
+  // })
+
+const routeGuardMain = async (
+  to: RouteLocationNormalized,
+  from: RouteLocationNormalized,
+  next: NavigationGuardNext
+) => {
+  if (to.path === "/main") {
+    next("/main/dashboard");
+  } else {
+    next();
+  }
+};
+</script>
+
 <template>
   <div>
-    <v-navigation-drawer persistent :rail="miniDrawer" v-model="showDrawer" >
-      <v-sheet  class="d-flex flex-column fill-height">
+    <v-navigation-drawer persistent :rail="miniDrawer" v-model="showDrawer">
+      <v-sheet class="d-flex flex-column fill-height">
         <v-sheet class="">
           <v-list>
-            <v-list-subheader><span v-show="!miniDrawer">Main menu</span></v-list-subheader>
+            <!-- <v-list-subheader><span v-show="!miniDrawer">Main menu</span></v-list-subheader> -->
             <v-list-item to="/main/dashboard" prepend-icon="dashboard">
-              <v-list-item-title>Dashboard</v-list-item-title>
+              <v-list-item-title>{{ t("dashboard") }}</v-list-item-title>
             </v-list-item>
             <v-list-item to="/main/make-video" prepend-icon="video_call">
-              <v-list-item-title>Make Video</v-list-item-title>
+              <v-list-item-title>{{ t("makeVideo") }}</v-list-item-title>
             </v-list-item>
             <v-list-item to="/main/progress" prepend-icon="list">
-              <v-list-item-title>Progress</v-list-item-title>
+              <v-list-item-title>{{ t("progress") }}</v-list-item-title>
             </v-list-item>
             <v-list-item to="/main/profile/view" prepend-icon="person">
-              <v-list-item-title>Profile</v-list-item-title>
+              <v-list-item-title>{{ t("userProfile") }}</v-list-item-title>
             </v-list-item>
             <v-list-item to="/main/profile/edit" prepend-icon="edit">
-              <v-list-item-title>Edit Profile</v-list-item-title>
+              <v-list-item-title>{{ t("editProfile") }}</v-list-item-title>
             </v-list-item>
             <v-list-item to="/main/profile/password" prepend-icon="key">
-              <v-list-item-title>Change Password</v-list-item-title>
+              <v-list-item-title>{{ t("changePassword") }}</v-list-item-title>
             </v-list-item>
           </v-list>
         </v-sheet>
         <v-divider></v-divider>
         <v-sheet class="">
           <v-list subheader v-show="hasAdminAccess">
-            <v-list-subheader><span v-show="!miniDrawer">Admin</span></v-list-subheader>
+            <v-list-subheader
+              ><span v-show="!miniDrawer">Admin</span></v-list-subheader
+            >
             <v-list-item to="/main/admin/users/all" prepend-icon="people">
               <v-list-item-title>Manage Users</v-list-item-title>
             </v-list-item>
-            <v-list-item to="/main/admin/users/create" prepend-icon="person_add">
+            <v-list-item
+              to="/main/admin/users/create"
+              prepend-icon="person_add"
+            >
               <v-list-item-title>Create User</v-list-item-title>
             </v-list-item>
-            <v-list-item to="/main/admin/test-celery" prepend-icon="engineering">
+            <v-list-item
+              to="/main/admin/test-celery"
+              prepend-icon="engineering"
+            >
               <v-list-item-title>Test Celery</v-list-item-title>
             </v-list-item>
           </v-list>
@@ -44,100 +121,76 @@
         <v-sheet class="mt-auto">
           <v-list>
             <v-list-item @click="logout" prepend-icon="logout">
-                <v-list-item-title>Logout</v-list-item-title>
-              </v-list-item>
+              <v-list-item-title>{{ t("logout") }}</v-list-item-title>
+            </v-list-item>
             <v-divider></v-divider>
-            <v-list-item @click="switchMiniDrawer" :prepend-icon="miniDrawer ? 'chevron_right' : 'chevron_left'">
-                <v-list-item-title>Collapse</v-list-item-title>
+            <v-list-item
+              @click="switchMiniDrawer"
+              :prepend-icon="miniDrawer ? 'chevron_right' : 'chevron_left'"
+            >
+              <v-list-item-title>{{ t("collapse") }}</v-list-item-title>
             </v-list-item>
           </v-list>
         </v-sheet>
       </v-sheet>
     </v-navigation-drawer>
     <v-main>
-    <v-toolbar dark color="primary" >
-      <v-app-bar-nav-icon @click.stop="switchShowDrawer"></v-app-bar-nav-icon>
-      <v-toolbar-title v-text="appName"></v-toolbar-title>
-      <v-spacer></v-spacer>
-      <v-menu bottom left offset-y>
-        <template v-slot:activator="{ props }">
-          <v-btn icon="more_vert" v-bind="props"/>
-        </template>
-        <v-list>
-          <v-list-item to="/main/profile" append-icon="person">
-              <v-list-item-title>Profile</v-list-item-title>
-          </v-list-item>
-          <v-list-item @click="logout" append-icon="logout">
-            <v-list-item-title>Logout</v-list-item-title>
+      <v-toolbar class="navbar">
+        <v-app-bar-nav-icon @click.stop="switchShowDrawer"></v-app-bar-nav-icon>
+        <v-toolbar-title v-text="appName"></v-toolbar-title>
+        <v-spacer></v-spacer>
+        <v-menu bottom left offset-y :close-on-content-click="false">
+          <template v-slot:activator="{ props }">
+            <v-btn icon="more_vert" v-bind="props" />
+          </template>
+          <v-list>
+            <v-list-item to="/main/profile" append-icon="person">
+              <v-list-item-title>{{ t("userProfile") }}</v-list-item-title>
+            </v-list-item>
+            <!-- <v-list-item to="/main/profile" append-icon="language">
+              <v-list-item-title>{{ t("language") }}</v-list-item-title>
+            </v-list-item> -->
 
+            <v-list-group value="Admin">
+              <template v-slot:activator="{ props }">
+                <v-list-item v-bind="props">
+                  <v-list-item-title>{{ t("language") }}</v-list-item-title>
+                </v-list-item>
+              </template>
+
+              <v-list-item
+            v-for="(item, index) in lang"
+            :key="index"
+            :value="item.text"
+            @click="setLang(item.text)"
+          >
+            <v-list-item-title>{{ item.title }}</v-list-item-title>
           </v-list-item>
-        </v-list>
-      </v-menu>
-    </v-toolbar>
+            </v-list-group>
+
+            <v-list-item @click="logout" append-icon="logout">
+              <v-list-item-title>{{ t("logout") }}</v-list-item-title>
+            </v-list-item>
+          </v-list>
+        </v-menu>
+      </v-toolbar>
       <router-view></router-view>
-  </v-main>
-    
+    </v-main>
+
     <v-footer class="pa-3" app>
       <v-spacer></v-spacer>
-      <span>&copy; {{appName}}</span>
+      <span>&copy; {{ appName }}</span>
     </v-footer>
   </div>
 </template>
 
-<script lang="ts">
-import { appName } from '@/env';
-import type { RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
-import { onBeforeRouteUpdate } from 'vue-router';
-import { useMainStore } from '@/stores/main';
-import { storeToRefs } from "pinia";
-
-
-export default{
-  setup(){
-    const mainStore = useMainStore();
-    const mainStoreRef = storeToRefs(mainStore);
-
-    const hasAdminAccess = mainStoreRef.readhasAdminAccess;
-
-    const miniDrawer = mainStoreRef.readDashboardMiniDrawer;
-    const showDrawer = mainStoreRef.readDashboardShowDrawer;
-
-    function switchMiniDrawer() {
-      mainStore.setDashboardMiniDrawer( !mainStoreRef.readDashboardMiniDrawer.value );
-    }
-
-    function switchShowDrawer() {
-      mainStore.setDashboardShowDrawer( !mainStoreRef.readDashboardShowDrawer.value );
-    }
-    
-    function logout(){
-      mainStore.logOut();
-    }
-
-    onBeforeRouteUpdate((to, from, next) =>  {
-      routeGuardMain(to, from, next);
-    });
-
-    return {
-      appName,
-      hasAdminAccess,
-      miniDrawer,
-      showDrawer,
-      switchMiniDrawer,
-      switchShowDrawer,
-      logout,
-    }
-  },
-  beforeRouteEnter(to, from, next){
-    routeGuardMain(to, from, next);
-  }
-};
-const routeGuardMain = async (to:RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
-  if (to.path === '/main') {
-    next('/main/dashboard');
-  } else {
-    next();
-  }
-};
-
-</script>
+<style lang="scss" scoped>
+.navbar {
+  color: #fff;
+  background-image: linear-gradient(
+    -225deg,
+    rgb(234, 84, 19) 35%,
+    rgb(178, 69, 146) 100%
+  );
+}
+</style>

+ 1 - 2
frontend/src/views/main/Start.vue

@@ -25,7 +25,6 @@
   
   //lifecycle
   
-
   //function
   const startRouteGuard = async (to:RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
     const mainStore = useMainStore();
@@ -33,7 +32,7 @@
     mainStore.checkLoggedIn();
     if (mainStoreRef.readIsLoggedIn.value) {
       if (to.path === '/login' || to.path === '/') {
-        next('/main');
+        next('/main/dashboard');
       } else {
         next();
       }

+ 1 - 1
frontend/tsconfig.app.json

@@ -1,6 +1,6 @@
 {
   "extends": "@vue/tsconfig/tsconfig.web.json",
-  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/**/*.json"],
   "exclude": ["src/**/__tests__/*"],
   "compilerOptions": {
     "composite": true,