Bläddra i källkod

Merge branch 'backend_models'

tomoya 2 år sedan
förälder
incheckning
4b9963c7fe

+ 68 - 0
backend/app/alembic/versions/208dd144a013_add_membership_progress_and_modify_user_.py

@@ -0,0 +1,68 @@
+"""add membership, progress and modify user and add video
+
+Revision ID: 208dd144a013
+Revises: d4867f3a4c0a
+Create Date: 2023-02-19 17:25:35.565786
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '208dd144a013'
+down_revision = 'd4867f3a4c0a'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('membership',
+    sa.Column('status', sa.String(length=20), nullable=False),
+    sa.PrimaryKeyConstraint('status')
+    )
+    op.create_table('progress',
+    sa.Column('state', sa.String(length=20), nullable=False),
+    sa.PrimaryKeyConstraint('state')
+    )
+    op.create_table('video',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('title', sa.String(), nullable=True),
+    sa.Column('progress_state', sa.String(length=20), nullable=True),
+    sa.Column('owner_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
+    sa.ForeignKeyConstraint(['progress_state'], ['progress.state'], onupdate='CASCADE', ondelete='RESTRICT'),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_index(op.f('ix_video_id'), 'video', ['id'], unique=False)
+    op.create_index(op.f('ix_video_title'), 'video', ['title'], unique=False)
+    op.add_column('user', sa.Column('membership_status', sa.String(length=20), nullable=True))
+    op.add_column('user', sa.Column('available_time', sa.Integer(), nullable=True))
+    op.alter_column('user', 'email',
+               existing_type=sa.VARCHAR(),
+               nullable=False)
+    op.alter_column('user', 'hashed_password',
+               existing_type=sa.VARCHAR(),
+               nullable=False)
+    op.create_foreign_key(None, 'user', 'membership', ['membership_status'], ['status'], onupdate='CASCADE', ondelete='RESTRICT')
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_constraint(None, 'user', type_='foreignkey')
+    op.alter_column('user', 'hashed_password',
+               existing_type=sa.VARCHAR(),
+               nullable=True)
+    op.alter_column('user', 'email',
+               existing_type=sa.VARCHAR(),
+               nullable=True)
+    op.drop_column('user', 'available_time')
+    op.drop_column('user', 'membership_status')
+    op.drop_index(op.f('ix_video_title'), table_name='video')
+    op.drop_index(op.f('ix_video_id'), table_name='video')
+    op.drop_table('video')
+    op.drop_table('progress')
+    op.drop_table('membership')
+    # ### end Alembic commands ###

+ 52 - 0
backend/app/alembic/versions/ad8a076d04a6_add_membership_progress_and_modify_user_.py

@@ -0,0 +1,52 @@
+"""add membership, progress and modify user and add video
+
+Revision ID: ad8a076d04a6
+Revises: 208dd144a013
+Create Date: 2023-02-19 18:29:18.179836
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'ad8a076d04a6'
+down_revision = '208dd144a013'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.alter_column('user', 'membership_status',
+               existing_type=sa.VARCHAR(length=20),
+               type_=sa.String(length=10),
+               existing_nullable=True)
+    op.add_column('video', sa.Column('stored_file_name', sa.String(), nullable=False))
+    op.alter_column('video', 'title',
+               existing_type=sa.VARCHAR(),
+               nullable=False)
+    op.alter_column('video', 'progress_state',
+               existing_type=sa.VARCHAR(length=20),
+               type_=sa.String(length=10),
+               existing_nullable=True)
+    op.create_unique_constraint(None, 'video', ['stored_file_name'])
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_constraint(None, 'video', type_='unique')
+    op.alter_column('video', 'progress_state',
+               existing_type=sa.String(length=10),
+               type_=sa.VARCHAR(length=20),
+               existing_nullable=True)
+    op.alter_column('video', 'title',
+               existing_type=sa.VARCHAR(),
+               nullable=True)
+    op.drop_column('video', 'stored_file_name')
+    op.alter_column('user', 'membership_status',
+               existing_type=sa.String(length=10),
+               type_=sa.VARCHAR(length=20),
+               existing_nullable=True)
+    # ### end Alembic commands ###

+ 4 - 0
backend/app/app/api/api_v1/endpoints/users.py

@@ -110,6 +110,10 @@ def create_user_open(
         )
         )
     user_in = schemas.UserCreate(password=password, email=email, full_name=full_name)
     user_in = schemas.UserCreate(password=password, email=email, full_name=full_name)
     user = crud.user.create(db, obj_in=user_in)
     user = crud.user.create(db, obj_in=user_in)
+    if settings.EMAILS_ENABLED and user_in.email:
+        send_new_account_email(
+            email_to=user_in.email, username=user_in.email, password=user_in.password
+        )
     return user
     return user
 
 
 
 

+ 63 - 0
backend/app/app/api/api_v1/endpoints/videos.py

@@ -0,0 +1,63 @@
+from typing import Any, List
+
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.orm import Session
+
+import app.crud as crud
+import app.models as models
+import app.schemas as schemas 
+from app.api import deps
+
+router = APIRouter()
+
+@router.get("/", response_model=List[schemas.Video])
+def get_video_list(
+    db: Session = Depends(deps.get_db),
+    skip: int = 0,
+    limit: int = 100,
+    current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Retrieve items.
+    """
+    if crud.user.is_superuser(current_user):
+        videos = crud.video.get_multi(db, skip=skip, limit=limit)
+    else:
+        videos = crud.video.get_multi_by_owner(
+            db=db, owner_id=current_user.id, skip=skip, limit=limit
+        )
+    return videos
+
+@router.post("/", response_model=schemas.Item)
+def upload_plot(
+    *,
+    db: Session = Depends(deps.get_db),
+    video_upload: schemas.VideoUpload,
+    current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Create new video.
+    """
+    video_create = schemas.VideoCreate
+    video_create.title = video_upload.title
+    video_create.progress = models.Progress.WAITING
+    video = crud.item.create_with_owner(db=db, obj_in=video_create, owner_id=current_user.id)
+    return video
+
+@router.get("/{id}")
+def download_video(
+
+) -> Any:
+    pass
+
+@router.get("/worker/{id}")
+def download_plot(
+
+) -> Any:
+    pass
+
+@router.get("/worker")
+def upload_complete_video(
+
+) -> Any:
+    pass

+ 3 - 0
backend/app/app/core/config.py

@@ -82,6 +82,9 @@ class Settings(BaseSettings):
     FIRST_SUPERUSER_PASSWORD: str
     FIRST_SUPERUSER_PASSWORD: str
     USERS_OPEN_REGISTRATION: bool = False
     USERS_OPEN_REGISTRATION: bool = False
 
 
+    MEMBERSHIP_TYPES: List[str]
+    PROGRESS_TYPES: List[str]
+
     class Config:
     class Config:
         case_sensitive = True
         case_sensitive = True
 
 

+ 1 - 0
backend/app/app/crud/__init__.py

@@ -1,5 +1,6 @@
 from .crud_item import item
 from .crud_item import item
 from .crud_user import user
 from .crud_user import user
+from .crud_video import video
 
 
 # For a new basic set of CRUD operations you could just do
 # For a new basic set of CRUD operations you could just do
 
 

+ 34 - 0
backend/app/app/crud/crud_video.py

@@ -0,0 +1,34 @@
+from typing import List
+
+from fastapi.encoders import jsonable_encoder
+from sqlalchemy.orm import Session
+
+from app.crud.base import CRUDBase
+from app.models.video import Video
+from app.schemas.video import VideoCreate, VideoUpdate
+
+
+class CRUDVideo(CRUDBase[Video, VideoCreate, VideoUpdate]):
+    def create_with_owner(
+        self, db: Session, *, obj_in: VideoCreate, owner_id: int
+    ) -> Video:
+        obj_in_data = jsonable_encoder(obj_in)
+        db_obj = self.model(**obj_in_data, owner_id=owner_id)
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    def get_multi_by_owner(
+        self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100
+    ) -> List[Video]:
+        return (
+            db.query(self.model)
+            .filter(Video.owner_id == owner_id)
+            .offset(skip)
+            .limit(limit)
+            .all()
+        )
+
+
+video = CRUDVideo(Video)

+ 2 - 0
backend/app/app/db/base.py

@@ -3,3 +3,5 @@
 from app.db.base_class import Base  # noqa
 from app.db.base_class import Base  # noqa
 from app.models.item import Item  # noqa
 from app.models.item import Item  # noqa
 from app.models.user import User  # noqa
 from app.models.user import User  # noqa
+from app.models.video import Video
+from app.models.enum import Progress, Membership

+ 13 - 1
backend/app/app/db/init_db.py

@@ -3,7 +3,7 @@ from sqlalchemy.orm import Session
 from app import crud, schemas
 from app import crud, schemas
 from app.core.config import settings
 from app.core.config import settings
 from app.db import base  # noqa: F401
 from app.db import base  # noqa: F401
-
+from app.models.enum import Membership, Progress
 # make sure all SQL Alchemy models are imported (app.db.base) before initializing DB
 # make sure all SQL Alchemy models are imported (app.db.base) before initializing DB
 # otherwise, SQL Alchemy might fail to initialize relationships properly
 # otherwise, SQL Alchemy might fail to initialize relationships properly
 # for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28
 # for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28
@@ -23,3 +23,15 @@ def init_db(db: Session) -> None:
             is_superuser=True,
             is_superuser=True,
         )
         )
         user = crud.user.create(db, obj_in=user_in)  # noqa: F841
         user = crud.user.create(db, obj_in=user_in)  # noqa: F841
+    
+    if settings.MEMBERSHIP_TYPES:
+        for TYPE in settings.MEMBERSHIP_TYPES:
+            if not db.query(Membership).filter(Membership.status == TYPE).first():
+                  db.add(Membership(status=TYPE))
+        db.commit()
+
+    if settings.PROGRESS_TYPES:
+        for TYPE in settings.PROGRESS_TYPES:
+            if not db.query(Progress).filter(Progress.state == TYPE).first():
+                db.add(Progress(state=TYPE))
+        db.commit()

+ 2 - 0
backend/app/app/models/__init__.py

@@ -1,2 +1,4 @@
 from .item import Item
 from .item import Item
 from .user import User
 from .user import User
+from .video import Video
+from .enum import Membership, Progress

+ 12 - 0
backend/app/app/models/enum.py

@@ -0,0 +1,12 @@
+from typing import TYPE_CHECKING
+
+from sqlalchemy import Column, ForeignKey, Integer, String
+from sqlalchemy.orm import relationship
+
+from app.db.base_class import Base
+
+class Membership(Base):
+  status = Column(String(10), primary_key=True)
+
+class Progress(Base):
+  state = Column(String(10), primary_key=True)

+ 11 - 0
backend/app/app/models/style.py

@@ -0,0 +1,11 @@
+from typing import TYPE_CHECKING
+
+from sqlalchemy import Column, ForeignKey, Integer, String
+from sqlalchemy.orm import relationship
+
+from app.db.base_class import Base
+
+class VideoStyle(Base):
+  id = Column(Integer, primary_key=True, index=True)
+  
+

+ 14 - 9
backend/app/app/models/user.py

@@ -1,19 +1,24 @@
 from typing import TYPE_CHECKING
 from typing import TYPE_CHECKING
 
 
-from sqlalchemy import Boolean, Column, Integer, String
+from sqlalchemy import Boolean, Column, Integer, String, ForeignKey
 from sqlalchemy.orm import relationship
 from sqlalchemy.orm import relationship
 
 
 from app.db.base_class import Base
 from app.db.base_class import Base
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from .item import Item  # noqa: F401
     from .item import Item  # noqa: F401
-
+    from .enum import Membership
 
 
 class User(Base):
 class User(Base):
-    id = Column(Integer, primary_key=True, index=True)
-    full_name = Column(String, index=True)
-    email = Column(String, unique=True, index=True, nullable=False)
-    hashed_password = Column(String, nullable=False)
-    is_active = Column(Boolean(), default=True)
-    is_superuser = Column(Boolean(), default=False)
-    items = relationship("Item", back_populates="owner")
+  id = Column(Integer, primary_key=True, index=True)
+  full_name = Column(String, index=True)
+  email = Column(String, unique=True, index=True, nullable=False)
+  hashed_password = Column(String, nullable=False)
+  membership_status = Column(String(10), 
+                      ForeignKey("membership.status", onupdate="CASCADE", ondelete="RESTRICT"), 
+                      default="normal")
+  available_time = Column(Integer, default=0)
+  is_active = Column(Boolean(), default=True)
+  is_superuser = Column(Boolean(), default=False)
+  items = relationship("Item", back_populates="owner")
+  videos = relationship("Video", back_populates="owner")

+ 21 - 0
backend/app/app/models/video.py

@@ -0,0 +1,21 @@
+from typing import TYPE_CHECKING
+
+from sqlalchemy import Column, ForeignKey, Integer, String, Enum
+from sqlalchemy.orm import relationship
+
+from app.db.base_class import Base
+
+
+if TYPE_CHECKING:
+  from .user import User  # noqa: F401
+  from .enum import Progress
+
+class Video(Base):
+  id = Column(Integer, primary_key=True, index=True)
+  title = Column(String, index=True, nullable=False)
+  stored_file_name = Column(String, unique=True, nullable=False)
+  progress_state = Column(String(10), 
+                    ForeignKey("progress.state", ondelete="RESTRICT", onupdate="CASCADE"),
+                    default="waiting")
+  owner_id = Column(Integer, ForeignKey("user.id"))
+  owner = relationship("User", back_populates="videos")

+ 1 - 0
backend/app/app/schemas/__init__.py

@@ -2,3 +2,4 @@ from .item import Item, ItemCreate, ItemInDB, ItemUpdate
 from .msg import Msg
 from .msg import Msg
 from .token import Token, TokenPayload
 from .token import Token, TokenPayload
 from .user import User, UserCreate, UserInDB, UserUpdate
 from .user import User, UserCreate, UserInDB, UserUpdate
+from .video import Video, VideoCreate, VideoInDB, VideoUpdate

+ 42 - 0
backend/app/app/schemas/video.py

@@ -0,0 +1,42 @@
+from typing import Optional
+
+from pydantic import BaseModel
+
+from fastapi import UploadFile, File
+
+# Shared properties
+class VideoBase(BaseModel):
+    title: Optional[str] = None
+
+# Properties to receive on item upload
+class VideoUpload(VideoBase):
+    title: str
+    zip_file: UploadFile=File()
+
+# Properties to receive on item creation
+class VideoCreate(VideoBase):
+    title: str
+
+# Properties to receive on item update
+class VideoUpdate(VideoBase):
+    pass
+
+# Properties shared by models stored in DB
+class VideoInDBBase(VideoBase):
+    id: int
+    title: str
+    progress: str
+    owner_id: int
+
+    class Config:
+        orm_mode = True
+
+
+# Properties to return to client
+class Video(VideoInDBBase):
+    pass
+
+
+# Properties properties stored in DB
+class VideoInDB(VideoInDBBase):
+    pass

+ 75 - 0
frontend/package-lock.json

@@ -12,6 +12,7 @@
         "pinia": "^2.0.28",
         "pinia": "^2.0.28",
         "sass": "^1.57.1",
         "sass": "^1.57.1",
         "vue": "^3.2.45",
         "vue": "^3.2.45",
+        "vue-i18n": "^9.2.2",
         "vue-router": "^4.1.6",
         "vue-router": "^4.1.6",
         "vuetify": "^3.1.1"
         "vuetify": "^3.1.1"
       },
       },
@@ -919,6 +920,63 @@
       "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
       "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
       "dev": true
       "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": {
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.1.1",
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
@@ -5571,6 +5629,23 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "dev": true
       "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": {
     "node_modules/vue-router": {
       "version": "4.1.6",
       "version": "4.1.6",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",
       "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",
     "pinia": "^2.0.28",
     "sass": "^1.57.1",
     "sass": "^1.57.1",
     "vue": "^3.2.45",
     "vue": "^3.2.45",
+    "vue-i18n": "^9.2.2",
     "vue-router": "^4.1.6",
     "vue-router": "^4.1.6",
     "vuetify": "^3.1.1"
     "vuetify": "^3.1.1"
   },
   },

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

@@ -1,12 +1,23 @@
 <script setup lang="ts">
 <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([
 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>
 </script>
 
 
 <template>
 <template>
@@ -21,9 +32,9 @@ let menu = reactive([
     <v-spacer></v-spacer>
     <v-spacer></v-spacer>
 
 
     <v-toolbar-items>
     <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>
       <v-menu>
         <template v-slot:activator="{ props }">
         <template v-slot:activator="{ props }">
           <v-btn color="gray" v-bind="props">
           <v-btn color="gray" v-bind="props">
@@ -45,9 +56,10 @@ let menu = reactive([
         </template>
         </template>
         <v-list>
         <v-list>
           <v-list-item
           <v-list-item
-            v-for="(item, index) in items"
+            v-for="(item, index) in lang"
             :key="index"
             :key="index"
-            :value="index"
+            :value="item.text"
+            @click="setLang(item.text)"
           >
           >
             <v-list-item-title>{{ item.title }}</v-list-item-title>
             <v-list-item-title>{{ item.title }}</v-list-item-title>
           </v-list-item>
           </v-list-item>

+ 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 App from "./App.vue";
 import { vuetify } from "./plugins/vuetify";
 import { vuetify } from "./plugins/vuetify";
 import { pinia } from "./plugins/pinia";
 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);
 const app = createApp(App);
 
 
 app.use(pinia);
 app.use(pinia);
 app.use(router);
 app.use(router);
 app.use(vuetify);
 app.use(vuetify);
+app.use(i18n);
 
 
 app.mount("#app");
 app.mount("#app");

+ 21 - 7
frontend/src/views/Login.vue

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

+ 70 - 64
frontend/src/views/Signup.vue

@@ -4,6 +4,7 @@ import { ref, reactive, computed, onMounted } from "vue";
 import { useMainStore } from "@/stores/main";
 import { useMainStore } from "@/stores/main";
 import { useDisplay } from "vuetify";
 import { useDisplay } from "vuetify";
 import { storeToRefs } from "pinia";
 import { storeToRefs } from "pinia";
+import { useI18n } from "vue-i18n";
 import Navbar from "@/components/Navbar.vue";
 import Navbar from "@/components/Navbar.vue";
 
 
 const mainStore = useMainStore();
 const mainStore = useMainStore();
@@ -13,10 +14,12 @@ const mainStoreRef = storeToRefs(mainStore);
 const email = ref("");
 const email = ref("");
 const password = ref("");
 const password = ref("");
 const { name } = useDisplay();
 const { name } = useDisplay();
+const { t } = useI18n();
 const confirmPassword = ref("");
 const confirmPassword = ref("");
 let data = reactive({
 let data = reactive({
   email: "",
   email: "",
   password: "",
   password: "",
+  full_name: ""
 });
 });
 let dialog = ref(false);
 let dialog = ref(false);
 let confirmState = ref(false);
 let confirmState = ref(false);
@@ -60,29 +63,29 @@ async function submit() {
         <section class="overflow-hidden banner-item">
         <section class="overflow-hidden banner-item">
           <img src="../assets/img/banner.png" alt="" />
           <img src="../assets/img/banner.png" alt="" />
           <h2>
           <h2>
-            將您的生活、創作、宣傳做成影片
+            {{ t("describe_1") }}
             <br />
             <br />
-            開始使用 AI Presentors
+            {{ t("describe_2") }}
           </h2>
           </h2>
         </section>
         </section>
       </v-col>
       </v-col>
       <v-col :cols="width" class="px-6 my-8 my-md-0">
       <v-col :cols="width" class="px-6 my-8 my-md-0">
         <div class="form-title">
         <div class="form-title">
-          <h3>註冊</h3>
+          <h3>{{ t("register") }}</h3>
           <span></span>
           <span></span>
         </div>
         </div>
         <v-form ref="form" class="login-form" lazy-validation>
         <v-form ref="form" class="login-form" lazy-validation>
-          <!-- <v-text-field
+          <v-text-field
             v-model="data.full_name"
             v-model="data.full_name"
             :rules="[(v) => !!v || '請輸入您的帳號']"
             :rules="[(v) => !!v || '請輸入您的帳號']"
             label="使用者名稱"
             label="使用者名稱"
             required
             required
-          ></v-text-field> -->
+          ></v-text-field>
 
 
           <v-text-field
           <v-text-field
             v-model="data.email"
             v-model="data.email"
             :rules="[(v) => !!v || '請輸入您的電子信箱']"
             :rules="[(v) => !!v || '請輸入您的電子信箱']"
-            label="電子信箱"
+            :label="$t('emailAddress')"
             required
             required
           ></v-text-field>
           ></v-text-field>
 
 
@@ -91,8 +94,8 @@ async function submit() {
             :append-icon="showPassword ? 'visibility' : 'visibility_off'"
             :append-icon="showPassword ? 'visibility' : 'visibility_off'"
             :rules="[(v) => !!v || '請輸入您的密碼']"
             :rules="[(v) => !!v || '請輸入您的密碼']"
             :type="showPassword ? 'text' : 'password'"
             :type="showPassword ? 'text' : 'password'"
-            label="設定密碼"
-            hint="4-12 位數密碼"
+            :label="$t('registerPassword')"
+            :hint="$t('passwordLength')"
             @click:append="showPassword = !showPassword"
             @click:append="showPassword = !showPassword"
             required
             required
           ></v-text-field>
           ></v-text-field>
@@ -102,8 +105,8 @@ async function submit() {
             :append-icon="showConfirmPassword ? 'visibility' : 'visibility_off'"
             :append-icon="showConfirmPassword ? 'visibility' : 'visibility_off'"
             :rules="[(v) => !!v || '請輸入您的密碼']"
             :rules="[(v) => !!v || '請輸入您的密碼']"
             :type="showConfirmPassword ? 'text' : 'password'"
             :type="showConfirmPassword ? 'text' : 'password'"
-            label="確認密碼"
-            hint="再次輸入您的密碼"
+            :label="$t('confirmPassword')"
+            :hint="$t('passwordConfirm')"
             @click:append="showConfirmPassword = !showConfirmPassword"
             @click:append="showConfirmPassword = !showConfirmPassword"
             required
             required
           ></v-text-field>
           ></v-text-field>
@@ -113,7 +116,8 @@ async function submit() {
           </v-alert>
           </v-alert>
 
 
           <p class="text-center">
           <p class="text-center">
-            已經有帳號? <router-link to="/login">登入</router-link>
+            {{ t("haveAccount") }}
+            <router-link to="/login"> {{ t("login") }}</router-link>
           </p>
           </p>
 
 
           <v-btn
           <v-btn
@@ -122,65 +126,67 @@ async function submit() {
             @click.prevent="submit"
             @click.prevent="submit"
             class="login-btn"
             class="login-btn"
           >
           >
-            立即註冊
+            {{ t("registerLink") }}
           </v-btn>
           </v-btn>
 
 
           <section
           <section
             class="mt-5 d-flex align-center justify-center dialog-content"
             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>
+            <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>
+                <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>
+                    是否經由本服務之使用下載或取得任何資料應由您自行考量且自負風險,並拋棄因前開任何資料之下載而導致您電腦系統、網路存取、下載或播放設備之任何損壞或資料流失,對
+                    ChoozMo 提出任何請求或採取法律行動,您應自負完全責任。
+                  </v-card-text>
+                </v-card>
+              </v-dialog>
+            </p>
           </section>
           </section>
         </v-form>
         </v-form>
       </v-col>
       </v-col>
@@ -228,7 +234,7 @@ async function submit() {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  font-size: 14px;
+  font-size: 13px;
   letter-spacing: 1px;
   letter-spacing: 1px;
   .v-btn {
   .v-btn {
     &:hover > .v-btn__overlay {
     &: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>
 <template>
   <v-container fluid>
   <v-container fluid>
     <v-card class="ma-3 pa-3">
     <v-card class="ma-3 pa-3">
@@ -16,22 +36,8 @@
   </v-container>
   </v-container>
 </template>
 </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>
 <template>
   <div>
   <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-sheet class="">
           <v-list>
           <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 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>
             <v-list-item to="/main/make-video" prepend-icon="video_call">
             <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>
             <v-list-item to="/main/progress" prepend-icon="list">
             <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>
             <v-list-item to="/main/profile/view" prepend-icon="person">
             <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>
             <v-list-item to="/main/profile/edit" prepend-icon="edit">
             <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>
             <v-list-item to="/main/profile/password" prepend-icon="key">
             <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-item>
           </v-list>
           </v-list>
         </v-sheet>
         </v-sheet>
         <v-divider></v-divider>
         <v-divider></v-divider>
         <v-sheet class="">
         <v-sheet class="">
           <v-list subheader v-show="hasAdminAccess">
           <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 to="/main/admin/users/all" prepend-icon="people">
               <v-list-item-title>Manage Users</v-list-item-title>
               <v-list-item-title>Manage Users</v-list-item-title>
             </v-list-item>
             </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-title>Create User</v-list-item-title>
             </v-list-item>
             </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-title>Test Celery</v-list-item-title>
             </v-list-item>
             </v-list-item>
           </v-list>
           </v-list>
@@ -44,100 +121,76 @@
         <v-sheet class="mt-auto">
         <v-sheet class="mt-auto">
           <v-list>
           <v-list>
             <v-list-item @click="logout" prepend-icon="logout">
             <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-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-item>
           </v-list>
           </v-list>
         </v-sheet>
         </v-sheet>
       </v-sheet>
       </v-sheet>
     </v-navigation-drawer>
     </v-navigation-drawer>
     <v-main>
     <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-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>
       <router-view></router-view>
-  </v-main>
-    
+    </v-main>
+
     <v-footer class="pa-3" app>
     <v-footer class="pa-3" app>
       <v-spacer></v-spacer>
       <v-spacer></v-spacer>
-      <span>&copy; {{appName}}</span>
+      <span>&copy; {{ appName }}</span>
     </v-footer>
     </v-footer>
   </div>
   </div>
 </template>
 </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
   //lifecycle
   
   
-
   //function
   //function
   const startRouteGuard = async (to:RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
   const startRouteGuard = async (to:RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
     const mainStore = useMainStore();
     const mainStore = useMainStore();
@@ -33,7 +32,7 @@
     mainStore.checkLoggedIn();
     mainStore.checkLoggedIn();
     if (mainStoreRef.readIsLoggedIn.value) {
     if (mainStoreRef.readIsLoggedIn.value) {
       if (to.path === '/login' || to.path === '/') {
       if (to.path === '/login' || to.path === '/') {
-        next('/main');
+        next('/main/dashboard');
       } else {
       } else {
         next();
         next();
       }
       }

+ 1 - 1
frontend/tsconfig.app.json

@@ -1,6 +1,6 @@
 {
 {
   "extends": "@vue/tsconfig/tsconfig.web.json",
   "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__/*"],
   "exclude": ["src/**/__tests__/*"],
   "compilerOptions": {
   "compilerOptions": {
     "composite": true,
     "composite": true,