web/satellite: show project invitations in all projects dashboard

This change allows users to view and interact with their project member
invitations from within the All Projects Dashboard.

References #5855

Change-Id: If5b4af46924530c91f8a5c16bfb5134de313dc90
This commit is contained in:
Jeremy Wharton 2023-06-01 14:06:50 -05:00
parent 6359c537c0
commit cafa6db971
19 changed files with 583 additions and 79 deletions

View File

@ -6,11 +6,13 @@ import {
DataStamp,
Project,
ProjectFields,
ProjectInvitation,
ProjectLimits,
ProjectsApi,
ProjectsCursor,
ProjectsPage,
ProjectsStorageBandwidthDaily,
ProjectInvitationResponse,
} from '@/types/projects';
import { HttpClient } from '@/utils/httpClient';
import { Time } from '@/utils/time';
@ -137,7 +139,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
* Get project limits.
*
* @param projectId- project ID
* throws Error
* @throws Error
*/
public async getLimits(projectId: string): Promise<ProjectLimits> {
const path = `${this.ROOT_PATH}/${projectId}/usage-limits`;
@ -165,7 +167,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
/**
* Get total limits for all the projects that user owns.
*
* throws Error
* @throws Error
*/
public async getTotalLimits(): Promise<ProjectLimits> {
const path = `${this.ROOT_PATH}/usage-limits`;
@ -192,7 +194,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
* @param projectId- project ID
* @param start- since date
* @param end- before date
* throws Error
* @throws Error
*/
public async getDailyUsage(projectId: string, start: Date, end: Date): Promise<ProjectsStorageBandwidthDaily> {
const since = Time.toUnixTimestamp(start).toString();
@ -202,7 +204,6 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
if (!response.ok) {
throw new Error('Can not get project daily usage');
}
const usage = await response.json();
@ -272,6 +273,45 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
return this.getProjectsPage(response.data.ownedProjects);
}
/**
* Returns a user's pending project member invitations.
*
* @throws Error
*/
public async getUserInvitations(): Promise<ProjectInvitation[]> {
const path = `${this.ROOT_PATH}/invitations`;
const response = await this.http.get(path);
const result = await response.json();
if (response.ok) {
return result.map(jsonInvite => new ProjectInvitation(
jsonInvite.projectID,
jsonInvite.projectName,
jsonInvite.projectDescription,
jsonInvite.inviterEmail,
new Date(jsonInvite.createdAt),
));
}
throw new Error(result.error || 'Failed to get project invitations');
}
/**
* Handles accepting or declining a user's project member invitation.
*
* @throws Error
*/
public async respondToInvitation(projectID: string, response: ProjectInvitationResponse): Promise<void> {
const path = `${this.ROOT_PATH}/invitations/${projectID}/respond`;
const body = { projectID, response };
const httpResponse = await this.http.post(path, JSON.stringify(body));
if (httpResponse.ok) return;
const result = await httpResponse.json();
throw new Error(result.error || 'Failed to respond to project invitation');
}
/**
* Method for mapping projects page from json to ProjectsPage type.
*
@ -294,5 +334,4 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
return new ProjectsPage(projects, page.limit, page.offset, page.pageCount, page.currentPage, page.totalCount);
}
}

View File

@ -41,6 +41,7 @@
<span class="label" :class="{uppercase: isUppercase}">
<component :is="iconComponent" v-if="iconComponent" />
<span v-if="icon !== 'none'">&nbsp;&nbsp;</span>
<slot />
{{ label }}
</span>
<div class="icon-wrapper-right">
@ -86,7 +87,7 @@ const props = withDefaults(defineProps<{
onPress?: () => void;
}>(), {
link: undefined,
label: 'Default',
label: '',
width: 'inherit',
height: 'inherit',
fontSize: '16px',

View File

@ -0,0 +1,172 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<VModal :on-close="closeModal">
<template #content>
<div class="modal">
<div class="modal__header">
<Icon />
<span class="modal__header__title">Join project</span>
</div>
<hr>
<div class="modal__info">
Join the {{ invite.projectName }} team project.
</div>
<hr>
<div class="modal__buttons">
<VButton
class="modal__buttons__button"
width="calc(50% - 8px)"
border-radius="8px"
font-size="14px"
:is-transparent="true"
:is-disabled="isLoading"
:on-press="() => respondToInvitation(ProjectInvitationResponse.Decline)"
label="Decline"
/>
<VButton
class="modal__buttons__button"
width="calc(50% - 8px)"
border-radius="8px"
font-size="14px"
:is-disabled="isLoading"
:on-press="() => respondToInvitation(ProjectInvitationResponse.Accept)"
label="Join Project"
/>
</div>
</div>
</template>
</VModal>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useNotify } from '@/utils/hooks';
import { ProjectInvitation, ProjectInvitationResponse } from '@/types/projects';
import { AnalyticsHttpApi } from '@/api/analytics';
import { LocalData } from '@/utils/localData';
import { RouteConfig } from '@/router';
import VModal from '@/components/common/VModal.vue';
import VButton from '@/components/common/VButton.vue';
import Icon from '@/../static/images/modals/boxesIcon.svg';
const appStore = useAppStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const router = useRouter();
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const isLoading = ref<boolean>(false);
/**
* Returns selected project member invitation from the store.
*/
const invite = computed((): ProjectInvitation => {
return projectsStore.state.selectedInvitation;
});
/**
* Handles accepting or declining the project member invitation.
*/
async function respondToInvitation(response: ProjectInvitationResponse): Promise<void> {
if (isLoading.value) return;
isLoading.value = true;
let success = false;
try {
await projectsStore.respondToInvitation(invite.value.projectID, response);
success = true;
} catch (error) {
const action = response === ProjectInvitationResponse.Accept ? 'accept' : 'decline';
notify.error(`Failed to ${action} project invitation. ${error.message}`, null);
}
try {
await projectsStore.getUserInvitations();
await projectsStore.getProjects();
} catch (error) {
notify.error(`Failed to reload projects and invitations list. ${error.message}`, null);
}
if (!success) {
isLoading.value = false;
return;
}
if (response === ProjectInvitationResponse.Accept) {
projectsStore.selectProject(invite.value.projectID);
LocalData.setSelectedProjectId(invite.value.projectID);
analytics.pageVisit(RouteConfig.ProjectDashboard.path);
router.push(RouteConfig.ProjectDashboard.path);
}
closeModal();
}
/**
* Closes modal.
*/
function closeModal(): void {
appStore.removeActiveModal();
}
</script>
<style scoped lang="scss">
.modal {
width: 410px;
padding: 32px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 16px;
@media screen and (width <= 460px) {
width: calc(100vw - 48px);
}
&__header {
display: flex;
gap: 16px;
align-items: center;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 24px;
line-height: 31px;
}
}
&__info {
font-family: 'font_regular', sans-serif;
font-size: 14px;
line-height: 20px;
text-align: left;
}
&__buttons {
display: flex;
gap: 16px;
justify-content: space-between;
&__button {
padding: 12px 0;
line-height: 24px;
}
}
& > hr {
height: 1px;
border: none;
background-color: var(--c-grey-2);
}
}
</style>

View File

@ -2,24 +2,39 @@
// See LICENSE for copying information.
<template>
<div class="tag" :class="{member: !isOwner}">
<box-icon v-if="!noIcon" class="tag__icon" />
<div class="tag" :class="{owner: isOwner, invited: isInvited}">
<component :is="icon" v-if="!noIcon" class="tag__icon" />
<span class="tag__text"> {{ isOwner ? 'Owner': 'Member' }} </span>
<span class="tag__text">{{ label }}</span>
</div>
</template>
<script setup lang="ts">
import BoxIcon from '@/../static/images/allDashboard/box.svg';
import { computed, Component } from 'vue';
import BoxIcon from '@/../static/images/navigation/project.svg';
import InviteIcon from '@/../static/images/navigation/quickStart.svg';
const props = withDefaults(defineProps<{
isOwner: boolean,
isInvited: boolean,
noIcon?: boolean,
}>(), {
isOwner: false,
isInvited: false,
noIcon: false,
});
const icon = computed((): string => {
return props.isInvited ? InviteIcon : BoxIcon;
});
const label = computed((): string => {
if (props.isOwner) return 'Owner';
if (props.isInvited) return 'Invited';
return 'Member';
});
</script>
<style scoped lang="scss">
@ -29,21 +44,39 @@ const props = withDefaults(defineProps<{
align-items: center;
gap: 5px;
padding: 4px 8px;
border: 1px solid var(--c-purple-2);
border: 1px solid var(--c-yellow-2);
border-radius: 24px;
color: var(--c-purple-4);
color: var(--c-yellow-5);
:deep(path) {
fill: var(--c-yellow-5);
}
&__icon {
width: 12px;
height: 12px;
}
&__text {
font-size: 12px;
font-family: 'font_regular', sans-serif;
}
&.member {
color: var(--c-yellow-5);
border-color: var(--c-yellow-2);
&.owner {
color: var(--c-purple-4);
border-color: var(--c-purple-2);
:deep(path) {
fill: var(--c-yellow-5);
fill: var(--c-purple-4);
}
}
&.invited {
color: var(--c-grey-6);
border-color: var(--c-grey-4);
:deep(path) {
fill: var(--c-yellow-3);
}
}
}

View File

@ -14,11 +14,14 @@ import {
ProjectsPage,
ProjectsStorageBandwidthDaily,
ProjectUsageDateRange,
ProjectInvitation,
ProjectInvitationResponse,
} from '@/types/projects';
import { ProjectsApiGql } from '@/api/projects';
import { DEFAULT_PAGE_LIMIT } from '@/types/pagination';
const defaultSelectedProject = new Project('', '', '', '', '', true, 0);
const defaultSelectedInvitation = new ProjectInvitation('', '', '', '', new Date());
export class ProjectsState {
public projects: Project[] = [];
@ -32,6 +35,8 @@ export class ProjectsState {
public storageChartData: DataStamp[] = [];
public chartDataSince: Date = new Date();
public chartDataBefore: Date = new Date();
public invitations: ProjectInvitation[] = [];
public selectedInvitation: ProjectInvitation = defaultSelectedInvitation;
}
export const useProjectsStore = defineStore('projects', () => {
@ -202,6 +207,22 @@ export const useProjectsStore = defineStore('projects', () => {
return await api.getSalt(projectID);
}
async function getUserInvitations(): Promise<ProjectInvitation[]> {
const invites = await api.getUserInvitations();
state.invitations = invites;
return invites;
}
async function respondToInvitation(projectID: string, response: ProjectInvitationResponse): Promise<void> {
await api.respondToInvitation(projectID, response);
}
function selectInvitation(invite: ProjectInvitation): void {
state.selectedInvitation = invite;
}
function clear(): void {
state.projects = [];
state.selectedProject = defaultSelectedProject;
@ -212,6 +233,8 @@ export const useProjectsStore = defineStore('projects', () => {
state.settledBandwidthChartData = [];
state.chartDataSince = new Date();
state.chartDataBefore = new Date();
state.invitations = [];
state.selectedInvitation = defaultSelectedInvitation;
}
function projectsCount(userID: string): number {
@ -257,6 +280,9 @@ export const useProjectsStore = defineStore('projects', () => {
getProjectLimits,
getTotalLimits,
getProjectSalt,
getUserInvitations,
respondToInvitation,
selectInvitation,
projectsCount,
clear,
projects,

View File

@ -43,7 +43,7 @@ export interface ProjectsApi {
* Get project limits.
*
* @param projectId- project ID
* throws Error
* @throws Error
*/
getLimits(projectId: string): Promise<ProjectLimits>;
@ -51,21 +51,21 @@ export interface ProjectsApi {
* Get project salt
*
* @param projectID - project ID
* throws Error
* @throws Error
*/
getSalt(projectID: string): Promise<string>;
/**
* Get project limits.
*
* throws Error
* @throws Error
*/
getTotalLimits(): Promise<ProjectLimits>;
/**
* Get project daily usage by specific date range.
*
* throws Error
* @throws Error
*/
getDailyUsage(projectID: string, start: Date, end: Date): Promise<ProjectsStorageBandwidthDaily>;
@ -76,6 +76,20 @@ export interface ProjectsApi {
* @throws Error
*/
getOwnedProjects(cursor: ProjectsCursor): Promise<ProjectsPage>;
/**
* Returns a user's pending project member invitations.
*
* @throws Error
*/
getUserInvitations(): Promise<ProjectInvitation[]>;
/**
* Handles accepting or declining a user's project member invitation.
*
* @throws Error
*/
respondToInvitation(projectID: string, response: ProjectInvitationResponse): Promise<void>;
}
/**
@ -223,6 +237,27 @@ export class ProjectsStorageBandwidthDaily {
) {}
}
/**
* ProjectInvitation represents a pending project member invitation.
*/
export class ProjectInvitation {
public constructor(
public projectID: string,
public projectName: string,
public projectDescription: string,
public inviterEmail: string,
public createdAt: Date,
) {}
}
/**
* ProjectInvitationResponse represents a response to a project member invitation.
*/
export enum ProjectInvitationResponse {
Decline,
Accept,
}
/**
* ProjectUsageDateRange is used to describe project's usage by date range.
*/

View File

@ -36,6 +36,7 @@ import EditSessionTimeoutModal from '@/components/modals/EditSessionTimeoutModal
import UpgradeAccountModal from '@/components/modals/upgradeAccountFlow/UpgradeAccountModal.vue';
import DeleteAccessGrantModal from '@/components/modals/DeleteAccessGrantModal.vue';
import SkipPassphraseModal from '@/components/modals/SkipPassphraseModal.vue';
import JoinProjectModal from '@/components/modals/JoinProjectModal.vue';
export const APP_STATE_DROPDOWNS = {
ACCOUNT: 'isAccountDropdownShown',
@ -86,6 +87,7 @@ enum Modals {
DELETE_ACCESS_GRANT = 'deleteAccessGrant',
SKIP_PASSPHRASE = 'skipPassphrase',
CHANGE_PROJECT_LIMIT = 'changeProjectLimit',
JOIN_PROJECT = 'joinProject',
}
export const MODALS: Record<Modals, Component> = {
@ -119,4 +121,5 @@ export const MODALS: Record<Modals, Component> = {
[Modals.DELETE_ACCESS_GRANT]: DeleteAccessGrantModal,
[Modals.SKIP_PASSPHRASE]: SkipPassphraseModal,
[Modals.CHANGE_PROJECT_LIMIT]: ChangeProjectLimitModal,
[Modals.JOIN_PROJECT]: JoinProjectModal,
};

View File

@ -42,9 +42,7 @@
border-radius="8px"
:is-disabled="isLoading"
:on-press="onActivateClick"
>
Reset Password
</v-button>
/>
<div class="activate-area__content-area__container__login-row">
<router-link :to="loginPath" class="activate-area__content-area__container__login-row__link">
Back to Login

View File

@ -73,9 +73,7 @@
border-radius="8px"
:is-disabled="isLoading"
:on-press="onSendConfigurations"
>
Reset Password
</v-button>
/>
<div class="forgot-area__content-area__container__login-container">
<router-link :to="loginPath" class="forgot-area__content-area__container__login-container__link">
Back to Login

View File

@ -123,9 +123,7 @@
border-radius="50px"
:is-disabled="isLoading"
:on-press="onLoginClick"
>
Sign In
</v-button>
/>
<span v-if="isMFARequired" class="login-area__content-area__container__cancel" :class="{ disabled: isLoading }" @click.prevent="onMFACancelClick">
Cancel
</span>

View File

@ -661,6 +661,12 @@ onMounted(async () => {
notify.error(`Unable to get credit cards. ${error.message}`, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
}
try {
await projectsStore.getUserInvitations();
} catch (error) {
notify.error(`Unable to get project invitations. ${error.message}`, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
}
try {
await projectsStore.getProjects();
} catch (error) {

View File

@ -5,7 +5,7 @@
<div class="empty-project-item">
<div class="empty-project-item__header">
<div class="empty-project-item__header__tag">
<box-icon />
<box-icon class="empty-project-item__header__tag__icon" />
<span> Project </span>
</div>
@ -40,7 +40,7 @@ import { useProjectsStore } from '@/store/modules/projectsStore';
import VButton from '@/components/common/VButton.vue';
import BoxIcon from '@/../static/images/allDashboard/box.svg';
import BoxIcon from '@/../static/images/navigation/project.svg';
const appStore = useAppStore();
const usersStore = useUsersStore();
@ -91,16 +91,20 @@ function onCreateProjectClicked(): void {
font-size: 12px;
font-family: 'font_regular', sans-serif;
& :deep(svg path) {
fill: var(--c-blue-4);
&__icon {
width: 12px;
height: 12px;
:deep(path) {
fill: var(--c-blue-4);
}
}
}
}
&__title {
margin-top: 16px;
font-weight: bold;
font-family: 'font_regular', sans-serif;
font-family: 'font_bold', sans-serif;
font-size: 24px;
line-height: 31px;
width: 100%;

View File

@ -15,8 +15,9 @@
/>
</div>
<div v-if="projects.length" class="my-projects__list">
<div v-if="projects.length || invites.length" class="my-projects__list">
<project-item v-for="project in projects" :key="project.id" :project="project" />
<project-invitation-item v-for="invite in invites" :key="invite.projectID" :invitation="invite" />
</div>
<div v-else class="my-projects__empty-area">
<empty-project-item class="my-projects__empty-area__item" />
@ -28,7 +29,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Project } from '@/types/projects';
import { Project, ProjectInvitation } from '@/types/projects';
import { RouteConfig } from '@/router';
import {
AnalyticsEvent,
@ -38,6 +39,7 @@ import { MODALS } from '@/utils/constants/appStatePopUps';
import { AnalyticsHttpApi } from '@/api/analytics';
import EmptyProjectItem from '@/views/all-dashboard/components/EmptyProjectItem.vue';
import ProjectItem from '@/views/all-dashboard/components/ProjectItem.vue';
import ProjectInvitationItem from '@/views/all-dashboard/components/ProjectInvitationItem.vue';
import { useUsersStore } from '@/store/modules/usersStore';
import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
@ -59,6 +61,14 @@ const projects = computed((): Project[] => {
return projectsStore.projects;
});
/**
* Returns project member invitations list from store.
*/
const invites = computed((): ProjectInvitation[] => {
return projectsStore.state.invitations.slice()
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
});
/**
* Route to create project page.
*/
@ -111,18 +121,18 @@ function onCreateProjectClicked(): void {
margin-top: 20px;
display: grid;
gap: 10px;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-template-columns: repeat(4, minmax(0, 1fr));
& :deep(.project-item) {
overflow: hidden;
}
@media screen and (width <= 1024px) {
grid-template-columns: 1fr 1fr 1fr;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media screen and (width <= 786px) {
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media screen and (width <= 425px) {

View File

@ -0,0 +1,163 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="invite-item">
<project-ownership-tag class="invite-item__tag" :is-invited="true" />
<div class="invite-item__info">
<p class="invite-item__info__name">{{ invitation.projectName }}</p>
<p class="invite-item__info__description">{{ invitation.projectDescription }}</p>
</div>
<div class="invite-item__buttons">
<VButton
class="invite-item__buttons__button"
width="fit-content"
border-radius="8px"
font-size="12px"
:is-disabled="isLoading"
:on-press="onJoinClicked"
>
<span class="invite-item__buttons__button__join-label">Join Project</span>
<span class="invite-item__buttons__button__join-label-mobile">Join</span>
</VButton>
<VButton
class="invite-item__buttons__button decline"
border-radius="8px"
font-size="12px"
:is-disabled="isLoading"
:is-transparent="true"
:on-press="onDeclineClicked"
label="Decline"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ProjectInvitation, ProjectInvitationResponse } from '@/types/projects';
import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import VButton from '@/components/common/VButton.vue';
import ProjectOwnershipTag from '@/components/project/ProjectOwnershipTag.vue';
const appStore = useAppStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const isLoading = ref<boolean>(false);
const props = withDefaults(defineProps<{
invitation?: ProjectInvitation,
}>(), {
invitation: () => new ProjectInvitation('', '', '', '', new Date()),
});
/**
* Displays the Join Project modal.
*/
function onJoinClicked(): void {
projectsStore.selectInvitation(props.invitation);
appStore.updateActiveModal(MODALS.joinProject);
}
/**
* Declines the project member invitation.
*/
async function onDeclineClicked(): Promise<void> {
if (isLoading.value) return;
isLoading.value = true;
try {
await projectsStore.respondToInvitation(props.invitation.projectID, ProjectInvitationResponse.Decline);
} catch (error) {
notify.error(`Failed to decline project invitation. ${error.message}`, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
}
try {
await projectsStore.getUserInvitations();
await projectsStore.getProjects();
} catch (error) {
notify.error(`Failed to reload projects and invitations list. ${error.message}`, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
}
isLoading.value = false;
}
</script>
<style scoped lang="scss">
.invite-item {
display: flex;
align-items: stretch;
flex-direction: column;
gap: 16px;
padding: 24px;
background: var(--c-white);
box-shadow: 0 0 20px rgb(0 0 0 / 5%);
border-radius: 8px;
&__tag {
align-self: flex-start;
}
&__info {
display: flex;
gap: 4px;
flex-direction: column;
&__name {
font-family: 'font_bold', sans-serif;
font-size: 24px;
line-height: 31px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
text-align: start;
}
&__description {
font-family: 'font_regular', sans-serif;
font-size: 14px;
min-height: 20px;
color: var(--c-grey-6);
line-height: 20px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
&__buttons {
display: flex;
gap: 10px;
&__button {
line-height: 20px;
padding: 10px 16px;
&__join-label-mobile {
display: none;
}
@media screen and (width <= 520px) and (width > 425px) {
&__join-label {
display: none;
}
&__join-label-mobile {
display: inline;
}
}
}
}
}
</style>

View File

@ -26,19 +26,16 @@
</div>
</div>
<p class="project-item__name">
{{ project.name }}
</p>
<p class="project-item__description">
{{ project.description }}
</p>
<div class="project-item__info">
<p class="project-item__info__name">{{ project.name }}</p>
<p class="project-item__info__description">{{ project.description }}</p>
</div>
<VButton
class="project-item__button"
width="fit-content"
height="fit-content"
border-radius="8px"
font-size="12px"
:on-press="onOpenClicked"
label="Open Project"
/>
@ -50,7 +47,6 @@ import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { Project } from '@/types/projects';
import { useNotify } from '@/utils/hooks';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { User } from '@/types/users';
import { AnalyticsHttpApi } from '@/api/analytics';
@ -75,7 +71,6 @@ const appStore = useAppStore();
const pmStore = useProjectMembersStore();
const usersStore = useUsersStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const router = useRouter();
const analytics = new AnalyticsHttpApi();
@ -165,11 +160,11 @@ async function goToProjectEdit(): Promise<void> {
<style scoped lang="scss">
.project-item {
display: grid;
grid-template-rows: 1fr 1fr 1fr 1fr;
align-items: start;
display: flex;
align-items: stretch;
flex-direction: column;
gap: 16px;
padding: 24px;
height: 200px;
background: var(--c-white);
box-shadow: 0 0 20px rgb(0 0 0 / 5%);
border-radius: 8px;
@ -231,30 +226,36 @@ async function goToProjectEdit(): Promise<void> {
}
}
&__name {
font-family: 'font_bold', sans-serif;
font-size: 24px;
line-height: 31px;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
text-align: start;
}
&__info {
display: flex;
gap: 4px;
flex-direction: column;
&__description {
font-family: 'font_regular', sans-serif;
font-size: 14px;
color: var(--c-grey-6);
line-height: 20px;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&__name {
font-family: 'font_bold', sans-serif;
font-size: 24px;
line-height: 31px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
text-align: start;
}
&__description {
font-family: 'font_regular', sans-serif;
font-size: 14px;
min-height: 20px;
color: var(--c-grey-6);
line-height: 20px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
&__button {
padding: 10px 16px;
line-height: 20px;
}
}
</style>

View File

@ -257,9 +257,7 @@
border-radius="50px"
:is-disabled="isLoading"
:on-press="onCreateClick"
>
Sign In
</v-button>
/>
<div class="register-area__input-area__login-container">
Already have an account? <router-link :to="loginPath" class="register-area__input-area__login-container__link">Login.</router-link>
</div>

View File

@ -1,3 +0,0 @@
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.13404 0.652105L5.15302 0.662491L9.44915 3.14357C9.59279 3.22653 9.66993 3.36706 9.68057 3.51186L9.68147 3.52864L9.68184 3.54736V8.46475C9.68184 8.62442 9.60006 8.77242 9.46612 8.85756L9.44836 8.86831L5.1522 11.3382C5.01422 11.4175 4.84577 11.4206 4.70545 11.3476L4.68649 11.3372L0.43205 8.86732C0.294547 8.7875 0.20772 8.64332 0.200723 8.48542L0.200195 8.46475L0.20035 3.53861L0.200195 3.52439C0.20104 3.37069 0.278044 3.22133 0.415856 3.13378L0.432684 3.12357L4.68716 0.66265C4.81842 0.586724 4.97752 0.579914 5.11378 0.642238L5.13404 0.652105ZM8.75081 4.35305L5.38566 6.29643V10.13L8.75081 8.19537V4.35305ZM1.13125 4.35694V8.19673L4.45468 10.1261V6.29507L1.13125 4.35694ZM4.92034 1.60319L1.57515 3.5381L4.92135 5.48952L8.28549 3.54665L4.92034 1.60319Z" fill="#7B61FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 887 B

View File

@ -0,0 +1,12 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4423 0H23.3463C28.8835 0 31.0805 0.613722 33.2353 1.76616C35.3902 2.91861 37.0814 4.60977 38.2338 6.76465L38.3214 6.93055C39.4029 9.0067 39.9846 11.2 40 16.4423V23.3463C40 28.8835 39.3863 31.0804 38.2338 33.2353C37.0814 35.3902 35.3902 37.0813 33.2353 38.2338L33.0694 38.3214C30.9933 39.4029 28.8 39.9845 23.5577 39.9999H16.6537C11.1165 39.9999 8.91954 39.3862 6.76466 38.2338C4.60977 37.0813 2.91861 35.3902 1.76617 33.2353L1.67858 33.0694C0.597074 30.9932 0.0154219 28.8 0 23.5576V16.6536C0 11.1165 0.613723 8.91953 1.76617 6.76465C2.91861 4.60977 4.60977 2.91861 6.76466 1.76616L6.93055 1.67858C9.00672 0.597073 11.2 0.0154218 16.4423 0Z" fill="#D8DEE3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.111086 13.1133L6.44892 16.7727V29.5423L0.0504854 25.8484C0.0193827 25.1545 0.00246402 24.3947 0 23.5571V16.6532C0 15.2862 0.037405 14.1227 0.111086 13.1133Z" fill="#FF458B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.4491 4.09766L17.508 10.4825L6.4491 16.8672L0.104614 13.2042C0.322552 10.1004 0.877608 8.43954 1.73786 6.81787L6.4491 4.09766Z" fill="#FFA3C5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.0001 16.4419V23.3459C40.0001 24.3675 39.9792 25.2753 39.9379 26.0902L34.1838 29.4118V16.6422L39.9048 13.3389C39.9646 14.2416 39.9966 15.2649 40.0001 16.4419Z" fill="#FF458B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.2791 3.9668L37.784 5.99021C37.942 6.24119 38.092 6.49934 38.2338 6.76451L38.3214 6.93041C39.1729 8.56503 39.7145 10.2722 39.9139 13.4831L34.2791 16.7364L23.2203 10.3516L34.2791 3.9668Z" fill="#FFA3C5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.2789 16.6426L34.2789 29.4122L23.2201 23.0274L23.2201 10.2578L34.2789 16.6426Z" fill="#DC0D5B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35376 16.7735L6.35376 29.5431L17.4126 23.1583L17.4126 10.3887L6.35376 16.7735Z" fill="#DC0D5B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.2509 12.0664L31.3097 18.4512L20.2509 24.8359L9.19202 18.4512L20.2509 12.0664Z" fill="#87A9FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.2506 24.7412L20.2506 37.5108L9.1919 31.126L9.19189 18.3564L20.2506 24.7412Z" fill="#0218A7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1556 24.7412L20.1556 37.5108L31.2144 31.126L31.2144 18.3564L20.1556 24.7412Z" fill="#0149FF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -4,6 +4,8 @@
import {
Project,
ProjectFields,
ProjectInvitation,
ProjectInvitationResponse,
ProjectLimits,
ProjectsApi,
ProjectsCursor,
@ -62,4 +64,12 @@ export class ProjectsApiMock implements ProjectsApi {
getDailyUsage(_projectId: string, _start: Date, _end: Date): Promise<ProjectsStorageBandwidthDaily> {
throw new Error('not implemented');
}
getUserInvitations(): Promise<ProjectInvitation[]> {
throw new Error('not implemented');
}
respondToInvitation(_projectID: string, _response: ProjectInvitationResponse): Promise<void> {
throw new Error('not implemented');
}
}