Skip to content

Commit

Permalink
enhance: advanced mode that allows user selects views, invite members (
Browse files Browse the repository at this point in the history
  • Loading branch information
hudy9x authored Feb 22, 2024
1 parent 8609bef commit 907f817
Show file tree
Hide file tree
Showing 14 changed files with 872 additions and 102 deletions.
349 changes: 274 additions & 75 deletions packages/be-gateway/src/routes/project/index.ts

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions packages/ui-app/app/_components/EmojiInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,39 @@ import EmojiPicker, { EmojiStyle, Categories } from 'emoji-picker-react'
import { useEffect, useRef, useState } from 'react'
import './style.css'

export const randIcon = () => {
const icons = [
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f98d.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f9a7.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f437.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f418.png',

'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f438.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f426.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f33b.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f332.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f341.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f951.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f955.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f344.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f96f.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f354.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f35f.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f36c.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f45b.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f451.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1fa73.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1fa95.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f941.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f4d5.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f4f0.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f4b5.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f4bc.png',
'https://cdn.jsdelivr.net/npm/emoji-datasource-twitter/img/twitter/64/1f4c1.png',
]

return icons[Math.round(Math.random() * icons.length - 1)]
}
export interface IEmojiInputProps {
className?: string
size?: 'sm' | 'base' | 'lg'
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-app/app/_components/ProjectIconPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function ProjectIconPicker({
}

return (
<div title="Click me to change icon">
<div className="shrink-0" title="Click me to change icon">
<EmojiInput
value={icon}
onChange={val => {
Expand Down
179 changes: 179 additions & 0 deletions packages/ui-app/app/_features/Project/Add/FormMembers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { OrgMember, useOrgMemberStore } from '@/store/orgMember'
import { useUser } from '@goalie/nextjs'
import { Avatar, Form, messageWarning } from '@shared/ui'
import { useEffect, useState } from 'react'
import { HiOutlineSearch } from 'react-icons/hi'
import { HiOutlineCheck } from 'react-icons/hi2'

function AddedMembers({ members }: { members: OrgMember[] }) {
return (
<div className="flex items-center justify-between">
<span className="text-[11px] uppercase text-gray-600 dark:text-gray-400">
Invited {members.length} members
</span>
<div className="py-2 flex items-center justify-end">
{members.slice(0, 3).map(m => {
return (
<div
key={m.id}
className="flex items-center -ml-2 shadow rounded-full">
<Avatar size="md" src={m.photo} name={m.name} />
</div>
)
})}

{members.length > 3 ? (
<div className="flex items-center justify-center bg-gray-200 dark:bg-gray-900 text-[10px] w-6 h-6 -ml-2 shadow rounded-full">
+{members.length - 3}
</div>
) : null}
</div>
</div>
)
}

function SelectMemberSection({
selection,
selected,
onClick
}: {
selected: string[]
selection: OrgMember[]
onClick: (isAlreadyAdded: boolean, m: OrgMember) => void
}) {
return (
<div className="border-t dark:border-gray-700 space-y-2 pt-2">
{selection.slice(0, 10).map(m => {
const isAdded = selected.includes(m.id)
return (
<div
key={m.id}
onClick={() => {
onClick(isAdded, m)
}}
className="flex items-center gap-2 cursor-pointer">
<Avatar size="md" src={m.photo} name={m.name} />
<div className="text-gray-700 dark:text-gray-400 flex items-center justify-between w-full">
<div>
<h2 className="text-sm">{m.name}</h2>
<p className="text-xs text-gray-400 dark:text-gray-600">
{m.email}
</p>
</div>
{isAdded ? <HiOutlineCheck /> : null}
</div>
</div>
)
})}
</div>
)
}

const useExtractAddedNSelection = (term: string, selected: string[]) => {
const { orgMembers } = useOrgMemberStore()
const added = orgMembers.filter(m => selected.includes(m.id))
const selection = orgMembers.filter(m => {
if (!term) return true

const name = m.name || ''
const email = m.email || ''

if (
name.toLowerCase().includes(term) ||
email.toLowerCase().includes(term)
) {
return true
}

return false
})

return {
added,
selection
}
}

export default function FormMember({
onChange
}: {
onChange?: (uids: string[]) => void
}) {
const { user } = useUser()
const [term, setTerm] = useState('')
const [selected, setSelected] = useState<string[]>([])

const { added, selection } = useExtractAddedNSelection(term, selected)

const triggerOnChange = (users: string[]) => {
onChange && onChange(users)
}

const addMember = (id: string) => {
setSelected(prev => {
const newMembers = [...prev, id]

triggerOnChange(newMembers)
return newMembers
})
}

const removeMember = (id: string) => {
if (id === user?.id) {
messageWarning('Please do not remove yourself =.=!! ')
return
}
setSelected(prev => {
// only being removed as the number of members are greater than 1

if (prev.length <= 1) {
return prev
}

const filtered = prev.filter(p => p !== id)
triggerOnChange(filtered)

return filtered
})
}

const onClick = (isAlreadyAdded: boolean, m: OrgMember) => {
if (isAlreadyAdded) {
removeMember(m.id)
return
}

addMember(m.id)
}

useEffect(() => {
if (user && user?.id) {
triggerOnChange([user.id])
setSelected(prev => [user.id, ...prev])
}
}, [user?.id])

return (
<div className="form-control">
<label>Invite members</label>
<div className="border p-2 rounded-md bg-gray-100 dark:bg-gray-800 dark:border-gray-700 space-y-2">
<div>
<Form.Input
onChange={ev => {
setTerm(ev.target.value)
}}
placeholder="Type name or email"
icon={<HiOutlineSearch className="text-gray-500" />}
/>
</div>

<AddedMembers members={added} />
<SelectMemberSection
onClick={onClick}
selection={selection}
selected={selected}
/>
</div>
</div>
)
}
90 changes: 90 additions & 0 deletions packages/ui-app/app/_features/Project/Add/FormProjectView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ProjectViewType } from '@prisma/client'
import { useEffect, useState } from 'react'

export default function FormProjectView({
onChange
}: {
onChange?: (views: ProjectViewType[]) => void
}) {
const views = [
{ type: ProjectViewType.BOARD, img: 'board', title: 'Board' },
{ type: ProjectViewType.LIST, img: 'list', title: 'List' },
{ type: ProjectViewType.CALENDAR, img: 'calendar', title: 'Calendar' },
{ type: ProjectViewType.GOAL, img: 'gantt', title: 'Goal' },
{ type: ProjectViewType.TEAM, img: 'team', title: 'Team' },
{ type: ProjectViewType.DASHBOARD, img: 'workload', title: 'Dashboard' }
]
const [selected, setSelected] = useState<number[]>([0])

useEffect(() => {
triggerOnChange([0])
}, [])

const triggerOnChange = (indexes: number[]) => {
const selectedViews: ProjectViewType[] = []
indexes.forEach(id => {
const v = views[id]
if (!v) return

selectedViews.push(v.type)
})

onChange && onChange(selectedViews)
}

const onSelect = (isSelected: boolean, vindex: number) => {
if (isSelected) {
setSelected(prev => {
if (prev.length === 1) return prev

const filtered = prev.filter(p => p !== vindex)
triggerOnChange(filtered)

return filtered
})
return
}
setSelected(prev => {
const newSelected = [...prev, ...[vindex]]
triggerOnChange(newSelected)

return newSelected
})
}

return (
<section className="form-control">
<label>Project view</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
{views.map((view, vindex) => {
const position = selected.findIndex(s => s === vindex)

const isSelected = selected.includes(vindex)
const classes = isSelected
? 'bg-indigo-50/50 border-indigo-300 dark:bg-indigo-300/30 dark:border-indigo-600'
: 'bg-gray-100 dark:bg-gray-800'
return (
<div
key={vindex}
onClick={() => {
onSelect(isSelected, vindex)
}}
className="cursor-pointer group">
<div className={`p-1 rounded-md ${classes} relative border-2 dark:border-gray-700`}>
<img src={`/project-view/${view.img}.svg`} />
{isSelected ? (
<span className="absolute top-1 right-1 text-[10px] p-1 rounded-full bg-gray-200 dark:bg-gray-900 border dark:border-gray-800 w-5 h-5 flex items-center justify-center ">
{position + 1}
</span>
) : null}
</div>
<h3 className="text-center text-[10px] font-medium uppercase mt-1">
{view.title}
</h3>
</div>
)
})}
</div>
</section>
)
}
Loading

0 comments on commit 907f817

Please sign in to comment.