diff --git a/core/Session/Session.go b/core/Session/Session.go index e4f952a..0bb1da8 100644 --- a/core/Session/Session.go +++ b/core/Session/Session.go @@ -21,3 +21,8 @@ func InitializeModule(newSession Session) *Session { } return sessionInstance } + +func (s *Session) UpdateCurrentUser(updatedUser User) User { + s.User = User(updatedUser) + return s.User +} diff --git a/core/Session/User.go b/core/Session/User.go index 524ab89..1af6d93 100644 --- a/core/Session/User.go +++ b/core/Session/User.go @@ -2,8 +2,10 @@ package session type User struct { Id string + LocalId string FirstName string LastName string AvatarPath string AuthToken string + Email string } diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 0f890a3..2509fdf 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -4,5 +4,6 @@ "@next/next/no-img-element": "off", "quotes": ["warn", "single"], "semi": ["warn", "never"] - } + }, + "ignorePatterns": ["wailsjs/*"] } diff --git a/frontend/components/project/Main.tsx b/frontend/components/project/Main.tsx index 84d71f3..ecce119 100644 --- a/frontend/components/project/Main.tsx +++ b/frontend/components/project/Main.tsx @@ -3,6 +3,8 @@ import { Popover, Transition } from '@headlessui/react' import { ChevronDownIcon, FolderArrowDownIcon, FolderOpenIcon, FolderPlusIcon } from '@heroicons/react/20/solid' import { Fragment, useState } from 'react' +import { useNavigation } from '../../context/Navigation/provider' +import { mainPages } from '../../context/Navigation/types' import { useProject } from '../../context/Project/provider' import NewProjectModal from './NewProjectModal' @@ -11,6 +13,7 @@ const MainProject = () => { const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false) const [canPopoverBeOpen, setCanPopoverBeOpen] = useState(true) const { createNewProject } = useProject() + const { setSelectedMainPage } = useNavigation() const buttonOptions = [ { @@ -44,6 +47,7 @@ const MainProject = () => { setIsNewProjectModalOpen(false) setCanPopoverBeOpen(true) createNewProject(projectName) + setSelectedMainPage(mainPages.WORKSPACE) } return
diff --git a/frontend/components/settings/User.tsx b/frontend/components/settings/User.tsx new file mode 100644 index 0000000..bb97fe9 --- /dev/null +++ b/frontend/components/settings/User.tsx @@ -0,0 +1,145 @@ +import Search from '../utils/Search' +import { useProject } from '../../context/Project/provider' +import { EnvelopeIcon } from '@heroicons/react/24/outline' +import UserAvatar from '../utils/UserAvatar' +import { useRef, useState } from 'react' + +const User = () => { + const { currentSession, requestUpdateCurrentUser, requestChooseUserAvatar } = useProject() + + const firstNameRef = useRef(null) + const lastNameRef = useRef(null) + const emailRef = useRef(null) + const [avatarPath, setAvatarPath] = useState(currentSession?.user?.avatarPath || '') + + const onSaveButtonClickHandler = () => { + requestUpdateCurrentUser({ + localId: currentSession?.user.localId, + firstName: firstNameRef?.current?.value, + lastName: lastNameRef?.current?.value, + email: emailRef?.current?.value, + avatarPath: avatarPath || '' + }) + } + + const onAvatarSelectButtonClickHandler = async () => { + const chosenAvatarPath = await requestChooseUserAvatar() + setAvatarPath(chosenAvatarPath) + } + + const onAvatarRemoveButtonClickHandler = () => { + setAvatarPath('') + } + + return
+ +
+
+
+
+ +
+
+

Profile

+

+ This information will be stored in a database if connected to a hosted account, so be careful what you share. +

+
+
+
+ + {/* ----- Name ----- */} +
+
Name
+
+ + +
+
+ {/* ----- Avatar ----- */} +
+
Avatar
+
+ + + + + + + + +
+
+ {/* ----- Email ----- */} +
+
Email
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+} + +export default User diff --git a/frontend/components/utils/Search.tsx b/frontend/components/utils/Search.tsx new file mode 100644 index 0000000..861aa34 --- /dev/null +++ b/frontend/components/utils/Search.tsx @@ -0,0 +1,102 @@ +import { Menu, Transition } from '@headlessui/react' +import { MagnifyingGlassIcon } from '@heroicons/react/20/solid' +import { BellIcon } from '@heroicons/react/24/outline' +import { Fragment } from 'react' +import { useNavigation } from '../../context/Navigation/provider' +import { mainPages } from '../../context/Navigation/types' +import UserAvatar from './UserAvatar' + +function classNames(...classes: any[]) { + return classes.filter(Boolean).join(' ') +} + +const Search = () => { + const { setSelectedMainPage } = useNavigation() + + const userNavigation = [ + { + name: 'Your Profile', + onClick: () => { setSelectedMainPage(mainPages.EDITUSER) } + }, + { + name: 'Document Workspace', + onClick: () => { setSelectedMainPage(mainPages.WORKSPACE) } + }, + { + name: 'Sign Out', + onClick: () => { setSelectedMainPage(mainPages.SELECTPROJECT) } + }, + ] + + return
+
+
+
+ +
+
+
+ +
+
+
+
+ + + {/* Profile dropdown */} + +
+ + Open user menu + + +
+ + + {userNavigation.map((item) => ( + + {({ active }) => ( + + {item.name} + + )} + + ))} + + +
+
+
+
+} + +export default Search \ No newline at end of file diff --git a/frontend/components/utils/UserAvatar.tsx b/frontend/components/utils/UserAvatar.tsx new file mode 100644 index 0000000..4bb57ed --- /dev/null +++ b/frontend/components/utils/UserAvatar.tsx @@ -0,0 +1,29 @@ +import { useProject } from '../../context/Project/provider' + +type Props = { overrideImagePath?: string } + +const UserAvatar = (props?: Props) => { + const { currentSession } = useProject() + + const avatarPath = props?.overrideImagePath ?? currentSession?.user?.avatarPath + + if (avatarPath) return user avatar + else if (currentSession?.user?.firstName || currentSession?.user?.lastName) return ( + + + {`${currentSession?.user?.firstName[0]}${currentSession?.user?.lastName[0]}`} + + + ) + else return + + + + +} + +export default UserAvatar \ No newline at end of file diff --git a/frontend/components/workspace/Search.tsx b/frontend/components/workspace/Search.tsx deleted file mode 100644 index 339e408..0000000 --- a/frontend/components/workspace/Search.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Menu, Transition } from '@headlessui/react' -import { MagnifyingGlassIcon } from '@heroicons/react/20/solid' -import { BellIcon } from '@heroicons/react/24/outline' -import { Fragment } from 'react' - -const userNavigation = [ - { name: 'Your Profile' }, - { name: 'Settings' }, - { name: 'Sign out' }, -] - -function classNames(...classes: any[]) { - return classes.filter(Boolean).join(' ') -} - -const Search = () =>
-
-
-
- -
-
-
- -
-
-
-
- - - {/* Profile dropdown */} - -
- - Open user menu - - -
- - - {userNavigation.map((item) => ( - - {({ active }) => ( - - {item.name} - - )} - - ))} - - -
-
-
-
- -export default Search \ No newline at end of file diff --git a/frontend/components/workspace/Sidebar.tsx b/frontend/components/workspace/Sidebar.tsx index e3a40c1..5218de3 100644 --- a/frontend/components/workspace/Sidebar.tsx +++ b/frontend/components/workspace/Sidebar.tsx @@ -390,7 +390,7 @@ function Sidebar() {
Textualize -

{currentSession.project.name}

+

{currentSession?.project?.name}

{renderNavigationItems()} diff --git a/frontend/components/workspace/TopBar.tsx b/frontend/components/workspace/TopBar.tsx index 11be45d..7f45c43 100644 --- a/frontend/components/workspace/TopBar.tsx +++ b/frontend/components/workspace/TopBar.tsx @@ -1,4 +1,4 @@ -import Search from './Search' +import Search from '../utils/Search' import ToolTabs from './ToolTabs' const TopBar = () =>
diff --git a/frontend/context/Navigation/provider.tsx b/frontend/context/Navigation/provider.tsx index 52eb033..1d4e4d6 100644 --- a/frontend/context/Navigation/provider.tsx +++ b/frontend/context/Navigation/provider.tsx @@ -1,8 +1,8 @@ 'use client' -import { createContext, ReactNode, useContext, useState } from 'react' +import { createContext, ReactNode, useContext, useEffect, useState } from 'react' import makeDefaultNavigation from './makeDefaultNavigation' -import { NavigationContextType, NavigationProps, workspaces } from './types' +import { mainPages, NavigationContextType, NavigationProps, workspaces } from './types' const NavigationContext = createContext(makeDefaultNavigation()) @@ -13,10 +13,13 @@ export function useNavigation() { type Props = { children: ReactNode, navigationProps: NavigationProps } export function NavigationProvidor({ children, navigationProps }: Props) { const [selectedWorkspace, setSelectedWorkspace] = useState(navigationProps.selectedWorkspace) + const [selectedMainPage, setSelectedMainPage] = useState(mainPages.SELECTPROJECT) const value = { selectedWorkspace, - setSelectedWorkspace + setSelectedWorkspace, + selectedMainPage, + setSelectedMainPage } return diff --git a/frontend/context/Navigation/types.ts b/frontend/context/Navigation/types.ts index 2e8d9cf..ac4e429 100644 --- a/frontend/context/Navigation/types.ts +++ b/frontend/context/Navigation/types.ts @@ -5,13 +5,22 @@ enum workspaces { DETAILS = 'DETAILS', } -export { workspaces } +enum mainPages { + WORKSPACE = 'WORKSPACE', + EDITUSER = 'EDITUSER', + SELECTPROJECT = 'SELECTPROJECT' +} + +export { workspaces, mainPages } export type NavigationContextType = { selectedWorkspace: workspaces, setSelectedWorkspace: (workspace: workspaces) => void + selectedMainPage: mainPages + setSelectedMainPage: (mainPage: mainPages) => void } export type NavigationProps = { selectedWorkspace: workspaces + selectedMainPage: mainPages } diff --git a/frontend/context/Project/makeDefaultProject.ts b/frontend/context/Project/makeDefaultProject.ts index 9c8d436..56cf22d 100644 --- a/frontend/context/Project/makeDefaultProject.ts +++ b/frontend/context/Project/makeDefaultProject.ts @@ -1,5 +1,5 @@ import { ipc } from '../../wailsjs/wailsjs/go/models' -import { ProjectContextType } from './types' +import { ProjectContextType, UserProps } from './types' const makeDefaultProject = (): ProjectContextType => ({ id: '', @@ -21,6 +21,8 @@ const makeDefaultProject = (): ProjectContextType => ({ setSelectedDocumentId: (id) => {}, currentSession: new ipc.Session(), createNewProject: (name: string) => Promise.resolve(new ipc.Session()), + requestUpdateCurrentUser: (updatedUserProps: UserProps) => Promise.resolve(new ipc.User()), + requestChooseUserAvatar: () => Promise.resolve(''), }) export default makeDefaultProject diff --git a/frontend/context/Project/provider.tsx b/frontend/context/Project/provider.tsx index 0b11928..5a28b1b 100644 --- a/frontend/context/Project/provider.tsx +++ b/frontend/context/Project/provider.tsx @@ -1,9 +1,15 @@ 'use client' import { createContext, ReactNode, useContext, useEffect, useState } from 'react' -import { CreateNewProject, GetCurrentSession, GetDocuments, GetProcessedAreasByDocumentId, GetUserMarkdownByDocumentId, RequestAddArea, RequestAddDocument, RequestAddDocumentGroup, RequestAddProcessedArea, RequestUpdateArea, RequestUpdateDocumentUserMarkdown } from '../../wailsjs/wailsjs/go/ipc/Channel' +import { + CreateNewProject, GetCurrentSession, GetDocuments, + GetProcessedAreasByDocumentId, GetUserMarkdownByDocumentId, RequestAddArea, + RequestAddDocument, RequestAddDocumentGroup, RequestAddProcessedArea, + RequestUpdateArea, RequestUpdateCurrentUser, RequestUpdateDocumentUserMarkdown, + RequestChooseUserAvatar, +} from '../../wailsjs/wailsjs/go/ipc/Channel' import { ipc } from '../../wailsjs/wailsjs/go/models' -import { AddAreaProps, AreaProps, ProjectContextType, ProjectProps } from './types' +import { AddAreaProps, AreaProps, ProjectContextType, ProjectProps, UserProps } from './types' import makeDefaultProject from './makeDefaultProject' const ProjectContext = createContext(makeDefaultProject()) @@ -14,9 +20,9 @@ export function useProject() { type Props = { children: ReactNode, projectProps: ProjectProps } export function ProjectProvider({ children, projectProps }: Props) { - const [ documents, setDocuments ] = useState(projectProps.documents) - const [ groups, setGroups ] = useState(projectProps.groups) - const [ selectedAreaId, setSelectedAreaId ] = useState('') + const [documents, setDocuments] = useState(projectProps.documents) + const [groups, setGroups] = useState(projectProps.groups) + const [selectedAreaId, setSelectedAreaId] = useState('') const [selectedDocumentId, setSelectedDocumentId] = useState('') const [currentSession, setCurrentSession] = useState(new ipc.Session()) @@ -54,7 +60,7 @@ export function ProjectProvider({ children, projectProps }: Props) { } const getAreaById = (areaId: string): ipc.Area | undefined => ( - documents.map(d => d.areas).flat().find(a => a.id ===areaId) + documents.map(d => d.areas).flat().find(a => a.id === areaId) ) const getSelectedDocument = () => documents.find(d => d.id === selectedDocumentId) @@ -106,6 +112,17 @@ export function ProjectProvider({ children, projectProps }: Props) { return sessionResponse } + const requestUpdateCurrentUser = async (userProps: UserProps) => { + const response = await RequestUpdateCurrentUser(new ipc.User(userProps)) + await updateSession() + return response + } + + const requestChooseUserAvatar = async () => { + const filePathResponse = await RequestChooseUserAvatar() + return filePathResponse + } + useEffect(() => { if (!documents.length && !groups.length) updateDocuments() }, [documents.length, groups.length]) @@ -130,9 +147,11 @@ export function ProjectProvider({ children, projectProps }: Props) { getUserMarkdownByDocumentId, currentSession, createNewProject, + requestUpdateCurrentUser, + requestChooseUserAvatar, } return - { children } + {children} } \ No newline at end of file diff --git a/frontend/context/Project/types.ts b/frontend/context/Project/types.ts index 73c564f..e7cf3fb 100644 --- a/frontend/context/Project/types.ts +++ b/frontend/context/Project/types.ts @@ -16,6 +16,16 @@ export type AddAreaProps = { export type AreaProps = { id: string } & AddAreaProps +export type UserProps = { + id?: string, + localId?: string, + firstName?: string, + lastName?: string, + avatarPath?: string, + authToken?: string, + email?: string +} + export type ProjectContextType = { getSelectedDocument: () => ipc.Document | undefined getAreaById: (areaId: string) => ipc.Area | undefined @@ -33,4 +43,6 @@ export type ProjectContextType = { setSelectedDocumentId: (id: string) => void currentSession: ipc.Session createNewProject: (name: string) => Promise + requestUpdateCurrentUser: (updatedUserProps: UserProps) => Promise + requestChooseUserAvatar: () => Promise } & ProjectProps \ No newline at end of file diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index e158015..1186d97 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,24 +1,34 @@ import { NextPage } from 'next' import MainHead from '../components/head' import MainProject from '../components/project/Main' +import User from '../components/settings/User' import MainWorkspace from '../components/workspace/Main' import Navigation from '../components/workspace/Navigation' +import { useNavigation } from '../context/Navigation/provider' +import { mainPages } from '../context/Navigation/types' import { useProject } from '../context/Project/provider' const Home: NextPage = () => { const { currentSession } = useProject() + const { selectedMainPage } = useNavigation() + + const renderSelectedMainPage = () => { + if (selectedMainPage === mainPages.SELECTPROJECT) return + else if (selectedMainPage === mainPages.EDITUSER) return + else if ((selectedMainPage === mainPages.WORKSPACE) && currentSession?.project?.id) { + return <> + + + + } + else return + } return ( <> - {!currentSession?.project?.id - ? - : <> - - - - } + { renderSelectedMainPage() } ) } diff --git a/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts b/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts index 1271a0f..e852876 100755 --- a/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts +++ b/frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts @@ -6,6 +6,8 @@ export function CreateNewProject(arg1:string):Promise; export function GetCurrentSession():Promise; +export function GetCurrentUser():Promise; + export function GetDocumentById(arg1:string):Promise; export function GetDocuments():Promise; @@ -22,6 +24,10 @@ export function RequestAddDocumentGroup(arg1:string):Promise; export function RequestAddProcessedArea(arg1:ipc.ProcessedArea):Promise; +export function RequestChooseUserAvatar():Promise; + export function RequestUpdateArea(arg1:ipc.Area):Promise; +export function RequestUpdateCurrentUser(arg1:ipc.User):Promise; + export function RequestUpdateDocumentUserMarkdown(arg1:string,arg2:string):Promise; diff --git a/frontend/wailsjs/wailsjs/go/ipc/Channel.js b/frontend/wailsjs/wailsjs/go/ipc/Channel.js index 533a3a6..f684885 100755 --- a/frontend/wailsjs/wailsjs/go/ipc/Channel.js +++ b/frontend/wailsjs/wailsjs/go/ipc/Channel.js @@ -10,6 +10,10 @@ export function GetCurrentSession() { return window['go']['ipc']['Channel']['GetCurrentSession'](); } +export function GetCurrentUser() { + return window['go']['ipc']['Channel']['GetCurrentUser'](); +} + export function GetDocumentById(arg1) { return window['go']['ipc']['Channel']['GetDocumentById'](arg1); } @@ -42,10 +46,18 @@ export function RequestAddProcessedArea(arg1) { return window['go']['ipc']['Channel']['RequestAddProcessedArea'](arg1); } +export function RequestChooseUserAvatar() { + return window['go']['ipc']['Channel']['RequestChooseUserAvatar'](); +} + export function RequestUpdateArea(arg1) { return window['go']['ipc']['Channel']['RequestUpdateArea'](arg1); } +export function RequestUpdateCurrentUser(arg1) { + return window['go']['ipc']['Channel']['RequestUpdateCurrentUser'](arg1); +} + export function RequestUpdateDocumentUserMarkdown(arg1, arg2) { return window['go']['ipc']['Channel']['RequestUpdateDocumentUserMarkdown'](arg1, arg2); } diff --git a/frontend/wailsjs/wailsjs/go/models.ts b/frontend/wailsjs/wailsjs/go/models.ts index 81a9394..392d4cd 100755 --- a/frontend/wailsjs/wailsjs/go/models.ts +++ b/frontend/wailsjs/wailsjs/go/models.ts @@ -115,10 +115,12 @@ export namespace ipc { export class User { id: string; + localId: string; firstName: string; lastName: string; avatarPath: string; authToken: string; + email: string; static createFrom(source: any = {}) { return new User(source); @@ -127,10 +129,12 @@ export namespace ipc { constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.id = source["id"]; + this.localId = source["localId"]; this.firstName = source["firstName"]; this.lastName = source["lastName"]; this.avatarPath = source["avatarPath"]; this.authToken = source["authToken"]; + this.email = source["email"]; } } export class Organization { diff --git a/ipc/JsonEntities.go b/ipc/JsonEntities.go index 5b7327d..64401b5 100644 --- a/ipc/JsonEntities.go +++ b/ipc/JsonEntities.go @@ -81,10 +81,12 @@ type UserMarkdownCollection struct { type User struct { Id string `json:"id"` + LocalId string `json:"localId"` FirstName string `json:"firstName"` LastName string `json:"lastName"` AvatarPath string `json:"avatarPath"` AuthToken string `json:"authToken"` + Email string `json:"email"` } type Organization struct { diff --git a/ipc/Session.go b/ipc/Session.go index 5e0edea..85df0e7 100644 --- a/ipc/Session.go +++ b/ipc/Session.go @@ -1,8 +1,11 @@ package ipc import ( + app "textualize/core/App" session "textualize/core/Session" + "github.com/wailsapp/wails/v2/pkg/runtime" + "github.com/google/uuid" ) @@ -36,3 +39,50 @@ func (c *Channel) CreateNewProject(name string) Session { return c.GetCurrentSession() } + +func (c *Channel) GetCurrentUser() User { + return User(session.GetInstance().User) +} + +func (c *Channel) RequestUpdateCurrentUser(updatedUserRequest User) User { + sessionInstance := session.GetInstance() + + user := session.User(sessionInstance.User) + + if user.LocalId == "" { + user.LocalId = uuid.NewString() + } + if updatedUserRequest.FirstName != "" { + user.FirstName = updatedUserRequest.FirstName + } + if updatedUserRequest.LastName != "" { + user.LastName = updatedUserRequest.LastName + } + if updatedUserRequest.Email != "" { + user.Email = updatedUserRequest.Email + } + + user.AvatarPath = updatedUserRequest.AvatarPath + + sessionInstance.UpdateCurrentUser(user) + return User(sessionInstance.User) +} + +func (c *Channel) RequestChooseUserAvatar() string { + filePath, err := runtime.OpenFileDialog(app.GetInstance().Context, runtime.OpenDialogOptions{ + Title: "Select an Image", + Filters: []runtime.FileFilter{ + { + DisplayName: "Image Files (*.jpg, *.png)", + Pattern: "*.jpg;*.png", + }, + }, + }) + + if err != nil { + runtime.LogError(app.GetInstance().Context, err.Error()) + return "" + } else { + return filePath + } +}