diff --git a/package.json b/package.json index 5ab239e..1e3b354 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulsar-web", - "version": "0.6.0", + "version": "0.7.0", "description": "", "type": "module", "scripts": { diff --git a/src/App.tsx b/src/App.tsx index d0ec5e8..059ddaa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { StartView } from "./views/StartView"; import { AppView } from "./views/AppView"; import { LoginView } from "./views/LoginView"; import { RegisterView } from "./views/RegisterView"; +import { InviteView } from "./views/InviteView"; const App: Component = () => { return ( @@ -12,6 +13,7 @@ const App: Component = () => { + ); }; diff --git a/src/api/channel/types.ts b/src/api/channel/types.ts index 58c94e8..fc50728 100644 --- a/src/api/channel/types.ts +++ b/src/api/channel/types.ts @@ -4,6 +4,8 @@ interface IFetchChannel { id: string; name: string; description?: string; + category: string; + order: number; communityId: string; creationDate: number; } @@ -25,6 +27,7 @@ interface IUpdateChannelRequest { id: string; name?: string; description?: string; + category?: string | null; } interface IUpdateChannelResponse extends IResponseSuccess, IFetchChannel {} diff --git a/src/api/community/community.ts b/src/api/community/community.ts index 135a623..e43b4e8 100644 --- a/src/api/community/community.ts +++ b/src/api/community/community.ts @@ -17,6 +17,14 @@ import { IFetchCommunityMembersResponse, IFetchCommunityInvitesRequest, IFetchCommunityInvitesResponse, + IUpdateCommunityChannelOrderRequest, + IUpdateCommunityChannelOrderResponse, + IUpdateCommunityRoleOrderRequest, + IUpdateCommunityRoleOrderResponse, + IRemoveCommunityMemberRequest, + IRemoveCommunityMemberResponse, + ICreateCommunityInviteRequest, + ICreateCommunityInviteResponse, } from "./types"; const fetchCommunityApi = async ( @@ -67,6 +75,45 @@ const fetchCommunityInvitesApi = async ( return await callApi(HTTP.GET, `community/${request.id}/invites`); }; +const updateCommunityChannelOrderApi = async ( + request: IUpdateCommunityChannelOrderRequest, +): Promise => { + return await callApi( + HTTP.PATCH, + `community/${request.id}/channels/order`, + request, + ); +}; + +const updateCommunityRoleOrderApi = async ( + request: IUpdateCommunityRoleOrderRequest, +): Promise => { + return await callApi( + HTTP.PATCH, + `community/${request.id}/roles/order`, + request, + ); +}; + +const removeCommunityMemberApi = async ( + request: IRemoveCommunityMemberRequest, +): Promise => { + return await callApi( + HTTP.DELETE, + `community/${request.id}/members/${request.memberId}`, + ); +}; + +const createCommunityInviteApi = async ( + request: ICreateCommunityInviteRequest, +): Promise => { + return await callApi( + HTTP.POST, + `community/${request.communityId}/invite`, + request, + ); +}; + export { fetchCommunityApi, createCommunityApi, @@ -76,4 +123,8 @@ export { fetchCommunityRolesApi, fetchCommunityMembersApi, fetchCommunityInvitesApi, + updateCommunityChannelOrderApi, + updateCommunityRoleOrderApi, + removeCommunityMemberApi, + createCommunityInviteApi, }; diff --git a/src/api/community/types.ts b/src/api/community/types.ts index f15a3e3..d73afb1 100644 --- a/src/api/community/types.ts +++ b/src/api/community/types.ts @@ -48,6 +48,9 @@ interface IFetchCommunityChannelsResponse extends IResponseSuccess { interface IFetchCommunityChannel { id: string; name: string; + description: string; + category: string; + order: number; } interface IFetchCommunityRolesRequest { @@ -62,6 +65,9 @@ interface IFetchCommunityRolesResponse extends IResponseSuccess { interface IFetchCommunityRole { id: string; name: string; + color: string; + order: number; + showInMembers: boolean; } interface IFetchCommunityMembersRequest { @@ -92,6 +98,45 @@ interface IFetchCommunityInvite { id: string; } +interface IUpdateCommunityChannelOrderRequest { + id: string; + order: string[]; +} + +interface IUpdateCommunityChannelOrderResponse extends IResponseSuccess { + id: string; +} + +interface IUpdateCommunityRoleOrderRequest { + id: string; + order: string[]; +} + +interface IUpdateCommunityRoleOrderResponse extends IResponseSuccess { + id: string; +} + +interface IRemoveCommunityMemberRequest { + id: string; + memberId: string; +} + +interface IRemoveCommunityMemberResponse extends IResponseSuccess { + id: string; + userId: string; +} + +interface ICreateCommunityInviteRequest { + communityId: string; + totalInvites?: number; + expirationDate?: number; +} + +interface ICreateCommunityInviteResponse extends IResponseSuccess { + id: string; + inviteId: string; +} + export { type IFetchCommunity, type IFetchCommunityRequest, @@ -114,4 +159,12 @@ export { type IFetchCommunityInvitesRequest, type IFetchCommunityInvitesResponse, type IFetchCommunityInvite, + type IUpdateCommunityChannelOrderRequest, + type IUpdateCommunityChannelOrderResponse, + type IUpdateCommunityRoleOrderRequest, + type IUpdateCommunityRoleOrderResponse, + type IRemoveCommunityMemberRequest, + type IRemoveCommunityMemberResponse, + type ICreateCommunityInviteRequest, + type ICreateCommunityInviteResponse, }; diff --git a/src/api/file/types.ts b/src/api/file/types.ts index 72245f3..5fc372e 100644 --- a/src/api/file/types.ts +++ b/src/api/file/types.ts @@ -17,6 +17,7 @@ interface IUploadCommunityAvatarResponse extends IResponseSuccess {} interface IFetchAttachment { id: string; + iv: string; filename: string; mimetype: string; size: number; @@ -33,6 +34,7 @@ interface IFetchAttachmentRequest { interface IFetchAttachmentResponse extends IResponseSuccess, IFetchAttachment {} interface ICreateAttachmentRequest { + iv: string; filename: string; mimetype: string; size: number; diff --git a/src/api/message/types.ts b/src/api/message/types.ts index e963ca3..754d0c6 100644 --- a/src/api/message/types.ts +++ b/src/api/message/types.ts @@ -32,6 +32,7 @@ interface ICreateMessageResponse extends IResponseSuccess, IFetchMessage {} interface IUpdateMessageRequest { id: string; + iv: string; text: string; } diff --git a/src/api/role/role.ts b/src/api/role/role.ts index 0c2a9e4..64a2c9b 100644 --- a/src/api/role/role.ts +++ b/src/api/role/role.ts @@ -9,6 +9,11 @@ import { IUpdateRoleResponse, IRemoveRoleRequest, IRemoveRoleResponse, + IAssignRoleRequest, + IAssignRoleResponse, + IUnssignRoleRequest, + IUnssignRoleResponse, + IFetchPermissionsResponse, } from "./types"; const fetchRoleApi = async ( @@ -35,4 +40,30 @@ const removeRoleApi = async ( return await callApi(HTTP.DELETE, `role/${request.id}`); }; -export { fetchRoleApi, createRoleApi, updateRoleApi, removeRoleApi }; +const assignRoleApi = async ( + request: IAssignRoleRequest, +): Promise => { + return await callApi(HTTP.POST, `role/${request.id}/assign`, request); +}; + +const unassignRoleApi = async ( + request: IUnssignRoleRequest, +): Promise => { + return await callApi(HTTP.POST, `role/${request.id}/unassign`, request); +}; + +const fetchPermissionsApi = async (): Promise< + IFetchPermissionsResponse | IResponseError +> => { + return await callApi(HTTP.GET, `role/permissions`); +}; + +export { + fetchRoleApi, + createRoleApi, + updateRoleApi, + removeRoleApi, + assignRoleApi, + unassignRoleApi, + fetchPermissionsApi, +}; diff --git a/src/api/role/types.ts b/src/api/role/types.ts index 60329ae..1bc4842 100644 --- a/src/api/role/types.ts +++ b/src/api/role/types.ts @@ -4,6 +4,9 @@ interface IFetchRole { id: string; name: string; description?: string; + color: string; + order: number; + showInMembers: boolean; communityId: string; permissions: string[]; creationDate: number; @@ -18,6 +21,7 @@ interface IFetchRoleResponse extends IResponseSuccess, IFetchRole {} interface ICreateRoleRequest { name: string; communityId: string; + permissions: string[]; } interface ICreateRoleResponse extends IResponseSuccess, IFetchRole {} @@ -25,6 +29,10 @@ interface ICreateRoleResponse extends IResponseSuccess, IFetchRole {} interface IUpdateRoleRequest { id: string; name?: string; + description?: string; + color?: string; + showInMembers?: boolean; + permissions?: string[]; } interface IUpdateRoleResponse extends IResponseSuccess, IFetchRole {} @@ -38,6 +46,34 @@ interface IRemoveRoleResponse extends IResponseSuccess { communityId: string; } +interface IAssignRoleRequest { + id: string; + userId: string; +} + +interface IAssignRoleResponse extends IResponseSuccess { + id: string; + name: string; + communityId: string; + userId: string; +} + +interface IUnssignRoleRequest { + id: string; + userId: string; +} + +interface IUnssignRoleResponse extends IResponseSuccess { + id: string; + name: string; + communityId: string; + userId: string; +} + +interface IFetchPermissionsResponse extends IResponseSuccess { + permissions: string[]; +} + export { type IFetchRole, type IFetchRoleRequest, @@ -48,4 +84,9 @@ export { type IUpdateRoleResponse, type IRemoveRoleResponse, type IRemoveRoleRequest, + type IAssignRoleRequest, + type IAssignRoleResponse, + type IUnssignRoleRequest, + type IUnssignRoleResponse, + type IFetchPermissionsResponse, }; diff --git a/src/api/user/types.ts b/src/api/user/types.ts index 3cac3e3..4819990 100644 --- a/src/api/user/types.ts +++ b/src/api/user/types.ts @@ -53,6 +53,23 @@ interface IFetchUserCommunity { name: string; } +interface IFetchUserCommunityRolesRequest { + id: string; + communityId: string; +} + +interface IFetchUserCommunityRolesResponse extends IResponseSuccess { + id: string; + communityId: string; + roles: IFetchUserCommunityRole[]; +} + +interface IFetchUserCommunityRole { + id: string; + name: string; + description: string; +} + interface IUpdateUserRequest { id: string; nickname?: string; @@ -73,6 +90,9 @@ export { type IFetchUserCommunitiesRequest, type IFetchUserCommunitiesResponse, type IFetchUserCommunity, + type IFetchUserCommunityRolesRequest, + type IFetchUserCommunityRolesResponse, + type IFetchUserCommunityRole, type IUpdateUserRequest, type IUpdateUserResponse, }; diff --git a/src/api/user/user.ts b/src/api/user/user.ts index cb5c632..dd2a68b 100644 --- a/src/api/user/user.ts +++ b/src/api/user/user.ts @@ -8,6 +8,8 @@ import { IFetchUserSessionsResponse, IFetchUserCommunitiesRequest, IFetchUserCommunitiesResponse, + IFetchUserCommunityRolesRequest, + IFetchUserCommunityRolesResponse, IUpdateUserRequest, IUpdateUserResponse, } from "./types"; @@ -36,6 +38,15 @@ const fetchUserCommunitiesApi = async ( return await callApi(HTTP.GET, `user/${request.id}/communities`); }; +const fetchUserCommunityRolesApi = async ( + request: IFetchUserCommunityRolesRequest, +): Promise => { + return await callApi( + HTTP.GET, + `user/${request.id}/community/${request.communityId}/roles`, + ); +}; + const updateUserApi = async ( request: IUpdateUserRequest, ): Promise => { @@ -47,5 +58,6 @@ export { fetchUserApi, fetchUserSessionsApi, fetchUserCommunitiesApi, + fetchUserCommunityRolesApi, updateUserApi, }; diff --git a/src/components/BadgeItem/BadgeItem.tsx b/src/components/BadgeItem/BadgeItem.tsx new file mode 100644 index 0000000..04d3579 --- /dev/null +++ b/src/components/BadgeItem/BadgeItem.tsx @@ -0,0 +1,23 @@ +import { createSignal, type Component } from "solid-js"; +import { IBadgeItemProps } from "./types"; +import { RemoveIcon, RemoveStrokeIcon } from "../../icons"; + +const BadgeItem: Component = (props: IBadgeItemProps) => { + const [getHover, setHover] = createSignal(false); + + return ( +
+ {props.text} +
props.onRemove?.(props.id)} + onMouseEnter={() => setHover(true)} + onMouseLeave={() => setHover(false)} + > + {getHover() ? : } +
+
+ ); +}; + +export { BadgeItem }; diff --git a/src/components/BadgeItem/index.ts b/src/components/BadgeItem/index.ts new file mode 100644 index 0000000..040de63 --- /dev/null +++ b/src/components/BadgeItem/index.ts @@ -0,0 +1,2 @@ +export * from "./BadgeItem"; +export * from "./types"; diff --git a/src/components/BadgeItem/types.ts b/src/components/BadgeItem/types.ts new file mode 100644 index 0000000..664f1e2 --- /dev/null +++ b/src/components/BadgeItem/types.ts @@ -0,0 +1,8 @@ +interface IBadgeItemProps { + id: string; + text: string; + removable: boolean; + onRemove?: (id: string) => void; +} + +export { type IBadgeItemProps }; diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index 815f9c2..9453fca 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -6,6 +6,7 @@ const Channel: Component = (props: IChannelProps) => {
  • props.onChannelClick?.(props.id)} + onContextMenu={(e) => props.onChannelRightClick?.(props.id, e)} >
     #
    {props.name}
    diff --git a/src/components/Channel/types.ts b/src/components/Channel/types.ts index 7a75709..3a48c38 100644 --- a/src/components/Channel/types.ts +++ b/src/components/Channel/types.ts @@ -3,6 +3,7 @@ interface IChannelProps { name: string; active: boolean; onChannelClick?: (id: string) => void; + onChannelRightClick?: (id: string, event: MouseEvent) => void; } export { type IChannelProps }; diff --git a/src/components/CheckItem/CheckItem.tsx b/src/components/CheckItem/CheckItem.tsx new file mode 100644 index 0000000..ba7118d --- /dev/null +++ b/src/components/CheckItem/CheckItem.tsx @@ -0,0 +1,15 @@ +import type { Component } from "solid-js"; +import { ICheckItemProps } from "./types"; + +const CheckItem: Component = (props: ICheckItemProps) => { + return ( +
    props.onClick?.(props.id, !props.checked)} + > + {props.text} +
    + ); +}; + +export { CheckItem }; diff --git a/src/components/CheckItem/index.ts b/src/components/CheckItem/index.ts new file mode 100644 index 0000000..844c647 --- /dev/null +++ b/src/components/CheckItem/index.ts @@ -0,0 +1,2 @@ +export * from "./CheckItem"; +export * from "./types"; diff --git a/src/components/CheckItem/types.ts b/src/components/CheckItem/types.ts new file mode 100644 index 0000000..c435c99 --- /dev/null +++ b/src/components/CheckItem/types.ts @@ -0,0 +1,8 @@ +interface ICheckItemProps { + id: string; + text: string; + checked: boolean; + onClick?: (id: string, checked: boolean) => void; +} + +export { type ICheckItemProps }; diff --git a/src/components/Community/Community.tsx b/src/components/Community/Community.tsx index 90a6204..c02e88b 100644 --- a/src/components/Community/Community.tsx +++ b/src/components/Community/Community.tsx @@ -7,6 +7,7 @@ const Community: Component = (props: ICommunityProps) => {
    props.onCommunityClick?.(props.id)} + onContextMenu={(e) => props.onCommunityRightClick?.(props.id, e)} >
    void; + onCommunityRightClick?: (id: string, event: MouseEvent) => void; } export { type ICommunityProps }; diff --git a/src/components/ContextMenu/ContextMenu.tsx b/src/components/ContextMenu/ContextMenu.tsx new file mode 100644 index 0000000..d54e383 --- /dev/null +++ b/src/components/ContextMenu/ContextMenu.tsx @@ -0,0 +1,54 @@ +import { createMemo, type Component, type JSXElement } from "solid-js"; +import { IContextMenuItem, IContextMenuProps } from "./types"; +import { Dynamic, Portal } from "solid-js/web"; + +const ContextMenu: Component = ( + props: IContextMenuProps, +) => { + let menuRef: HTMLUListElement | undefined; + + const isRight = createMemo(() => { + const offsetWidth = props.offset ?? 0; + + return props.position.x + offsetWidth >= window.innerWidth; + }); + + const mapItem = (item: IContextMenuItem): JSXElement => { + return ( +
  • +
    + +
    + {item.label} +
  • + ); + }; + + return ( + <> + {props.visible && props.items.length > 0 ? ( + +
      + {props.items.map(mapItem)} +
    +
    + ) : undefined} + + ); +}; + +export { ContextMenu }; diff --git a/src/components/ContextMenu/index.ts b/src/components/ContextMenu/index.ts new file mode 100644 index 0000000..7405156 --- /dev/null +++ b/src/components/ContextMenu/index.ts @@ -0,0 +1,3 @@ +export * from "./ContextMenu"; +export * from "./types"; +export * from "./useContextMenu"; diff --git a/src/components/ContextMenu/types.ts b/src/components/ContextMenu/types.ts new file mode 100644 index 0000000..1d5a1d2 --- /dev/null +++ b/src/components/ContextMenu/types.ts @@ -0,0 +1,23 @@ +import { JSXElement } from "solid-js"; +import { IconParameters } from "../../icons"; + +interface IContextMenuProps { + visible: boolean; + position: IPosition; + offset?: number; + items: IContextMenuItem[]; +} + +interface IPosition { + x: number; + y: number; +} + +interface IContextMenuItem { + label: string; + icon: (props: IconParameters) => JSXElement; + color?: string; + onClick?: () => void; +} + +export { type IContextMenuProps, type IPosition, type IContextMenuItem }; diff --git a/src/components/ContextMenu/useContextMenu.ts b/src/components/ContextMenu/useContextMenu.ts new file mode 100644 index 0000000..5d46535 --- /dev/null +++ b/src/components/ContextMenu/useContextMenu.ts @@ -0,0 +1,44 @@ +import { createSignal, onCleanup } from "solid-js"; +import { resetContextMenuOpenId, setContextMenuOpenId } from "../../store/app"; +import { state } from "../../store/state"; + +const useContextMenu = (id: string) => { + const [getData, setData] = createSignal(undefined); + const [getPos, setPos] = createSignal({ x: 0, y: 0 }); + + const getVisible = (): boolean => { + return state.app.contextMenuOpenId === id; + }; + + const open = (e: MouseEvent, data: T) => { + e.preventDefault(); + e.stopPropagation(); + + setData(data as Exclude); + setPos({ x: e.clientX, y: e.clientY }); + setContextMenuOpenId(id); + }; + + const close = () => resetContextMenuOpenId(); + + const handleGlobal = (e: MouseEvent | KeyboardEvent) => { + if (e instanceof KeyboardEvent && e.key === "Escape") { + close(); + } + if (e instanceof MouseEvent) { + close(); + } + }; + + window.addEventListener("click", handleGlobal); + window.addEventListener("keydown", handleGlobal); + + onCleanup(() => { + window.removeEventListener("click", handleGlobal); + window.removeEventListener("keydown", handleGlobal); + }); + + return { getData, getVisible, getPos, open, close }; +}; + +export { useContextMenu }; diff --git a/src/components/FileInput/FileInput.tsx b/src/components/FileInput/FileInput.tsx index 3a54d5b..e04cc1c 100644 --- a/src/components/FileInput/FileInput.tsx +++ b/src/components/FileInput/FileInput.tsx @@ -32,7 +32,7 @@ const FileInput: Component = (props: IFileInputProps) => { class={`bg-stone-800 h-40 w-40 p-2 ${props.rounded ? "rounded-full" : "rounded-2xl"} ${props.outline ? "outline-2" : ""}`} >