web/satellite: add components for new dashboard

This change just adds the components/changes needed by the all projects
dashboard.

Issue: https://github.com/storj/storj/issues/5514

Change-Id: Ie7926f563dd3eb47a56b234cfd004f5b69be00a8
This commit is contained in:
Wilfred Asomani 2023-02-20 21:37:18 +00:00 committed by Storj Robot
parent 85d6843f2e
commit 76d7bc6d18
11 changed files with 1258 additions and 4 deletions

View File

@ -3,7 +3,32 @@
<template>
<!-- if isDisabled check onPress in parent element -->
<a
v-if="link"
:href="link"
:class="containerClassName"
:style="style"
tabindex="0"
target="_blank"
rel="noopener noreferrer"
@click="onPress"
>
<slot name="icon" />
<div v-if="isWhiteGreen" class="greenCheck">&#x2713;</div>
<div v-if="isGreenWhite" class="whiteCheck">&#x2713;</div>
<span class="label" :class="{uppercase: isUppercase}">
<CopyIcon v-if="icon.toLowerCase() === 'copy'" />
<LockIcon v-if="icon.toLowerCase() === 'lock'" />
<CreditCardIcon v-if="icon.toLowerCase() === 'credit-card'" />
<DocumentIcon v-if="icon.toLowerCase() === 'document'" />
<TrashIcon v-if="icon.toLowerCase() === 'trash'" />
<FolderIcon v-if="icon.toLowerCase() === 'folder'" />
<resources-icon v-if="icon.toLowerCase() === 'resources'" />
<add-circle-icon v-if="icon.toLowerCase() === 'addcircle'" />
<span v-if="icon !== 'none'">&nbsp;&nbsp;</span>{{ label }}</span>
</a>
<div
v-else
:class="containerClassName"
:style="style"
tabindex="0"
@ -21,6 +46,8 @@
<DocumentIcon v-if="icon.toLowerCase() === 'document'" />
<TrashIcon v-if="icon.toLowerCase() === 'trash'" />
<FolderIcon v-if="icon.toLowerCase() === 'folder'" />
<resources-icon v-if="icon.toLowerCase() === 'resources'" />
<add-circle-icon v-if="icon.toLowerCase() === 'addcircle'" />
<span v-if="icon !== 'none'">&nbsp;&nbsp;</span>{{ label }}</span>
</div>
</template>
@ -29,6 +56,7 @@
import { computed } from 'vue';
import AddCircleIcon from '@/../static/images/common/addCircle.svg';
import CopyIcon from '@/../static/images/common/copyButtonIcon.svg';
import TrashIcon from '@/../static/images/accessGrants/trashIcon.svg';
import LockIcon from '@/../static/images/common/lockIcon.svg';
@ -36,8 +64,10 @@ import CreditCardIcon from '@/../static/images/common/creditCardIcon-white.svg';
import DocumentIcon from '@/../static/images/common/documentIcon.svg';
import DownloadIcon from '@/../static/images/common/download.svg';
import FolderIcon from '@/../static/images/objects/newFolder.svg';
import ResourcesIcon from '@/../static/images/navigation/resources.svg';
const props = withDefaults(defineProps<{
link?: string;
label?: string;
width?: string;
height?: string;
@ -57,6 +87,7 @@ const props = withDefaults(defineProps<{
isUppercase?: boolean;
onPress?: () => void;
}>(), {
link: undefined,
label: 'Default',
width: 'inherit',
height: 'inherit',

View File

@ -2,7 +2,7 @@
// See LICENSE for copying information.
<template>
<VModal :on-close="closeModal">
<VModal :on-close="() => closeModal(true)">
<template #content>
<div class="modal">
<EnterPassphraseIcon />
@ -25,7 +25,7 @@
height="48px"
font-size="14px"
:is-transparent="true"
:on-press="closeModal"
:on-press="() => closeModal()"
:is-disabled="isLoading"
/>
<VButton
@ -52,6 +52,7 @@ import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { AnalyticsHttpApi } from '@/api/analytics';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { RouteConfig } from '@/router';
import VModal from '@/components/common/VModal.vue';
import VInput from '@/components/common/VInput.vue';
@ -176,11 +177,16 @@ export default class EnterPassphraseModal extends Vue {
}
/**
* Closes open bucket modal.
* Closes enter passphrase modal and navigates to single project dashboard from
* all projects dashboard.
*/
public closeModal(): void {
public closeModal(isCloseButton = false): void {
if (this.isLoading) return;
if (!isCloseButton && this.$route.name === RouteConfig.AllProjectsDashboard.name) {
this.$router.push(RouteConfig.ProjectDashboard.path);
}
this.$store.commit(APP_STATE_MUTATIONS.REMOVE_ACTIVE_MODAL);
}

View File

@ -62,6 +62,7 @@ Vue.use(Router);
export abstract class RouteConfig {
// root paths
public static Root = new NavigationLink('/', 'Root');
public static AllProjectsDashboard = new NavigationLink('/all', 'All Projects');
public static Login = new NavigationLink('/login', 'Login');
public static Register = new NavigationLink('/signup', 'Register');
public static RegisterSuccess = new NavigationLink('/signup-success', 'RegisterSuccess');

View File

@ -30,6 +30,7 @@ import EnterPassphraseModal from '@/components/modals/EnterPassphraseModal.vue';
export const APP_STATE_DROPDOWNS = {
ACCOUNT: 'isAccountDropdownShown',
ALL_DASH_ACCOUNT: 'allProjectsDashboardAccount',
SELECT_PROJECT: 'isSelectProjectDropdownShown',
RESOURCES: 'isResourcesDropdownShown',
QUICK_START: 'isQuickStartDropdownShown',

View File

@ -0,0 +1,464 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="header">
<div class="header__content">
<LogoIcon class="header__content__logo" />
<div class="header__content__actions">
<VButton
class="header__content__actions__docs"
icon="resources"
is-white
:link="link"
:on-press="sendDocsEvent"
label="Go to Docs"
/>
<my-account-button />
</div>
</div>
<div class="header__mobile-area">
<div class="header__mobile-area__container">
<header class="header__mobile-area__container__header">
<div class="header__mobile-area__container__header__logo" @click.stop="goToProjects">
<LogoIcon />
</div>
<CrossIcon v-if="isNavOpened" @click="toggleNavigation" />
<MenuIcon v-else @click="toggleNavigation" />
</header>
<div v-if="isNavOpened" class="header__mobile-area__container__wrap">
<a
aria-label="Docs"
class="header__mobile-area__container__wrap__item-container"
:href="link"
target="_blank"
rel="noopener noreferrer"
@click="sendDocsEvent"
>
<div class="header__mobile-area__container__wrap__item-container__left">
<resources-icon class="header__mobile-area__container__wrap__item-container__left__image" />
<p class="header__mobile-area__container__wrap__item-container__left__label">Go to Docs</p>
</div>
</a>
<div class="header__mobile-area__container__wrap__border" />
<div class="account-area">
<div class="account-area__wrap" aria-roledescription="account-area" @click.stop="toggleAccountDropdown">
<div class="account-area__wrap__left">
<AccountIcon class="account-area__wrap__left__icon" />
<p class="account-area__wrap__left__label">My Account</p>
<p class="account-area__wrap__left__label-small">Account</p>
<TierBadgePro v-if="user.paidTier" class="account-area__wrap__left__tier-badge" />
<TierBadgeFree v-else class="account-area__wrap__left__tier-badge" />
</div>
<ArrowIcon class="account-area__wrap__arrow" />
</div>
<div v-if="isAccountDropdownShown" class="account-area__dropdown">
<div class="account-area__dropdown__header">
<div class="account-area__dropdown__header__left">
<SatelliteIcon />
<h2 class="account-area__dropdown__header__left__label">Satellite</h2>
</div>
<div class="account-area__dropdown__header__right">
<p class="account-area__dropdown__header__right__sat">{{ satellite }}</p>
<a
class="account-area__dropdown__header__right__link"
href="https://docs.storj.io/dcs/concepts/satellite"
target="_blank"
rel="noopener noreferrer"
>
<InfoIcon />
</a>
</div>
</div>
<div class="account-area__dropdown__item" @click="navigateToBilling">
<BillingIcon />
<p class="account-area__dropdown__item__label">Billing</p>
</div>
<div class="account-area__dropdown__item" @click="navigateToSettings">
<SettingsIcon />
<p class="account-area__dropdown__item__label">Account Settings</p>
</div>
<div class="account-area__dropdown__item" @click="onLogout">
<LogoutIcon />
<p class="account-area__dropdown__item__label">Logout</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useNotify, useRoute, useRouter, useStore } from '@/utils/hooks';
import MyAccountButton from '@/views/all-dashboard/components/MyAccountButton.vue';
import {
AnalyticsErrorEventSource,
AnalyticsEvent,
} from '@/utils/constants/analyticsEventNames';
import { AnalyticsHttpApi } from '@/api/analytics';
import { RouteConfig } from '@/router';
import { User } from '@/types/users';
import {
APP_STATE_ACTIONS,
NOTIFICATION_ACTIONS,
PM_ACTIONS,
} from '@/utils/constants/actionNames';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { USER_ACTIONS } from '@/store/modules/users';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { AB_TESTING_ACTIONS } from '@/store/modules/abTesting';
import { AuthHttpApi } from '@/api/auth';
import { MetaUtils } from '@/utils/meta';
import VButton from '@/components/common/VButton.vue';
import LogoIcon from '@/../static/images/logo.svg';
import ResourcesIcon from '@/../static/images/navigation/resources.svg';
import BillingIcon from '@/../static/images/navigation/billing.svg';
import LogoutIcon from '@/../static/images/navigation/logout.svg';
import SettingsIcon from '@/../static/images/navigation/settings.svg';
import TierBadgeFree from '@/../static/images/navigation/tierBadgeFree.svg';
import TierBadgePro from '@/../static/images/navigation/tierBadgePro.svg';
import InfoIcon from '@/../static/images/navigation/info.svg';
import SatelliteIcon from '@/../static/images/navigation/satellite.svg';
import AccountIcon from '@/../static/images/navigation/account.svg';
import ArrowIcon from '@/../static/images/navigation/arrowExpandRight.svg';
import CrossIcon from '@/../static/images/common/closeCross.svg';
import MenuIcon from '@/../static/images/navigation/menu.svg';
const router = useRouter();
const route = useRoute();
const store = useStore();
const analytics = new AnalyticsHttpApi();
const auth = new AuthHttpApi();
const notify = useNotify();
const link = 'https://docs.storj.io/';
const isAccountDropdownShown = ref(false);
const isNavOpened = ref(false);
/**
* Returns satellite name from store.
*/
const satellite = computed((): string => {
return store.state.appStateModule.satelliteName;
});
/**
* Returns user from store.
*/
const user = computed((): User => {
return store.getters.user;
});
/**
* Indicates if new billing screen should be used.
*/
const isNewBillingScreen = computed((): boolean => {
return MetaUtils.getMetaContent('new-billing-screen') === 'true';
});
/**
* Toggles account dropdown visibility.
*/
function toggleAccountDropdown(): void {
isAccountDropdownShown.value = !isAccountDropdownShown.value;
}
/**
* Toggles navigation content visibility.
*/
function toggleNavigation(): void {
isNavOpened.value = !isNavOpened.value;
}
/**
* Sends "View Docs" event to segment and opens link.
*/
function goToProjects(): void {
analytics.pageVisit(RouteConfig.AllProjectsDashboard.path);
router.push(RouteConfig.AllProjectsDashboard.path);
}
function navigateToBilling(): void {
toggleNavigation();
const routeConf = isNewBillingScreen ?
RouteConfig.AccountSettings.with(RouteConfig.Billing).with(RouteConfig.BillingOverview).path :
RouteConfig.AccountSettings.with(RouteConfig.Billing).path;
router.push(routeConf);
analytics.pageVisit(routeConf);
}
/**
* Navigates user to account settings page.
*/
function navigateToSettings(): void {
toggleNavigation();
analytics.pageVisit(RouteConfig.AccountSettings.with(RouteConfig.Settings).path);
router.push(RouteConfig.AccountSettings.with(RouteConfig.Settings).path).catch(() => {return;});
}
/**
* Logouts user and navigates to login page.
*/
async function onLogout(): Promise<void> {
analytics.pageVisit(RouteConfig.Login.path);
await router.push(RouteConfig.Login.path);
await Promise.all([
store.dispatch(PM_ACTIONS.CLEAR),
store.dispatch(PROJECTS_ACTIONS.CLEAR),
store.dispatch(USER_ACTIONS.CLEAR),
store.dispatch(ACCESS_GRANTS_ACTIONS.STOP_ACCESS_GRANTS_WEB_WORKER),
store.dispatch(ACCESS_GRANTS_ACTIONS.CLEAR),
store.dispatch(NOTIFICATION_ACTIONS.CLEAR),
store.dispatch(BUCKET_ACTIONS.CLEAR),
store.dispatch(OBJECTS_ACTIONS.CLEAR),
store.dispatch(APP_STATE_ACTIONS.CLEAR),
store.dispatch(PAYMENTS_ACTIONS.CLEAR_PAYMENT_INFO),
store.dispatch(AB_TESTING_ACTIONS.RESET),
store.dispatch('files/clear'),
]);
try {
analytics.eventTriggered(AnalyticsEvent.LOGOUT_CLICKED);
await auth.logout();
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.NAVIGATION_ACCOUNT_AREA);
}
}
/**
* Sends "View Docs" event to segment and opens link.
*/
function sendDocsEvent(): void {
analytics.pageVisit(link);
analytics.eventTriggered(AnalyticsEvent.VIEW_DOCS_CLICKED);
}
</script>
<style scoped lang="scss">
.header {
&__content {
display: flex;
justify-content: space-between;
align-items: center;
&__logo {
cursor: pointer;
min-height: 37px;
width: 207px;
height: 37px;
}
&__actions {
display: flex;
gap: 10px;
&__docs {
padding: 10px 16px;
border-radius: 8px;
}
}
}
&__mobile-area {
background-color: #fff;
font-family: 'font_regular', sans-serif;
box-shadow: 0 0 32px rgb(0 0 0 / 4%);
display: none;
&__container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
overflow-x: hidden;
overflow-y: auto;
width: 100%;
&__header {
display: flex;
width: 100%;
box-sizing: border-box;
padding: 0 32px;
justify-content: space-between;
align-items: center;
height: 4rem;
&__logo {
width: 211px;
max-width: 211px;
height: 37px;
max-height: 37px;
svg {
width: 211px;
height: 37px;
}
}
}
&__wrap {
position: fixed;
top: 4rem;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
z-index: 9999;
overflow-y: auto;
overflow-x: hidden;
background: white;
&__item-container {
padding: 14px 32px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
border-left: 4px solid #fff;
color: var(--c-grey-6);
position: static;
cursor: pointer;
height: 48px;
box-sizing: border-box;
&__left {
display: flex;
align-items: center;
&__label {
font-size: 14px;
line-height: 20px;
margin-left: 24px;
}
}
}
&__border {
margin: 0 32px 16px;
border: 0.5px solid var(--c-grey-2);
width: calc(100% - 48px);
}
}
}
}
@media screen and (max-width: 500px) {
&__content {
display: none;
}
&__mobile-area {
display: block;
}
}
}
.account-area {
width: 100%;
&__wrap {
box-sizing: border-box;
padding: 16px 32px;
height: 48px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
position: static;
&__left {
display: flex;
align-items: center;
justify-content: space-between;
&__label,
&__label-small {
font-size: 14px;
line-height: 20px;
color: var(--c-grey-6);
margin: 0 6px 0 24px;
}
&__label-small {
display: none;
margin: 0;
}
}
}
&__dropdown {
position: relative;
background: #fff;
width: 100%;
box-sizing: border-box;
&__header {
background: var(--c-grey-1);
padding: 16px 32px;
border: 1px solid var(--c-grey-2);
display: flex;
align-items: center;
justify-content: space-between;
&__left,
&__right {
display: flex;
align-items: center;
&__label {
font-size: 14px;
line-height: 20px;
color: var(--c-grey-6);
margin-left: 16px;
}
&__sat {
font-size: 14px;
line-height: 20px;
color: var(--c-grey-6);
margin-right: 16px;
}
&__link {
max-height: 16px;
}
}
}
&__item {
display: flex;
align-items: center;
padding: 16px 32px;
background: var(--c-grey-1);
&__label {
margin-left: 16px;
font-size: 14px;
line-height: 20px;
color: var(--c-grey-6);
}
&:last-of-type {
border-radius: 0 0 8px 8px;
}
}
}
}
</style>

View File

@ -0,0 +1,343 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div
class="account-button"
@click.stop.prevent="openDropdown"
@mouseenter="isHoveredOver = true"
@mouseleave="isHoveredOver = false"
>
<div class="account-button__button">
<account-icon class="account-button__button__icon" :class="{active: isHoveredOver || isDropdownOpen}" />
<span class="account-button__button__label" :class="{active: isHoveredOver || isDropdownOpen}">My Account</span>
<arrow-down-icon class="account-button__arrow" :class="{active: isDropdownOpen, hovered: isHoveredOver}" />
</div>
<div v-if="isDropdownOpen" v-click-outside="closeDropdown" class="account-button__dropdown">
<div class="account-button__dropdown__header">
<div class="account-button__dropdown__header__left">
<SatelliteIcon />
<h2 class="account-button__dropdown__header__left__label">Account Region</h2>
</div>
<div class="account-button__dropdown__header__right">
<p class="account-button__dropdown__header__right__sat">{{ satellite }}</p>
<a
href="https://docs.storj.io/dcs/concepts/satellite"
target="_blank"
rel="noopener noreferrer"
class="account-button__dropdown__header__right__link"
@click.stop="closeDropdown"
>
<InfoIcon />
</a>
</div>
</div>
<div tabindex="0" class="account-button__dropdown__item" @click.stop="navigateToBilling" @keyup.enter="navigateToBilling">
<BillingIcon />
<p class="account-button__dropdown__item__label">Billing</p>
</div>
<div tabindex="0" class="account-button__dropdown__item" @click.stop="navigateToSettings" @keyup.enter="navigateToSettings">
<SettingsIcon />
<p class="account-button__dropdown__item__label">Account Settings</p>
</div>
<div tabindex="0" class="account-button__dropdown__item" @click.stop="onLogout" @keyup.enter="onLogout">
<LogoutIcon />
<p class="account-button__dropdown__item__label">Logout</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { RouteConfig } from '@/router';
import { useNotify, useRoute, useRouter, useStore } from '@/utils/hooks';
import {
APP_STATE_ACTIONS,
NOTIFICATION_ACTIONS,
PM_ACTIONS,
} from '@/utils/constants/actionNames';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { USER_ACTIONS } from '@/store/modules/users';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { AB_TESTING_ACTIONS } from '@/store/modules/abTesting';
import {
AnalyticsErrorEventSource,
AnalyticsEvent,
} from '@/utils/constants/analyticsEventNames';
import { MetaUtils } from '@/utils/meta';
import { AnalyticsHttpApi } from '@/api/analytics';
import { AuthHttpApi } from '@/api/auth';
import { APP_STATE_DROPDOWNS } from '@/utils/constants/appStatePopUps';
import AccountIcon from '@/../static/images/navigation/account.svg';
import ArrowDownIcon from '@/../static/images/common/dropIcon.svg';
import LogoutIcon from '@/../static/images/navigation/logout.svg';
import SatelliteIcon from '@/../static/images/navigation/satellite.svg';
import InfoIcon from '@/../static/images/navigation/info.svg';
import BillingIcon from '@/../static/images/navigation/billing.svg';
import SettingsIcon from '@/../static/images/navigation/settings.svg';
const router = useRouter();
const route = useRoute();
const store = useStore();
const notify = useNotify();
const analytics = new AnalyticsHttpApi();
const auth = new AuthHttpApi();
const isHoveredOver = ref(false);
/**
* Indicates if account dropdown is open.
*/
const isDropdownOpen = computed((): boolean => {
return store.state.appStateModule.appState.activeDropdown === APP_STATE_DROPDOWNS.ALL_DASH_ACCOUNT;
});
/**
* Indicates if new billing screen should be used.
*/
const isNewBillingScreen = computed((): boolean => {
return MetaUtils.getMetaContent('new-billing-screen') === 'true';
});
/**
* Returns satellite name from store.
*/
const satellite = computed((): string => {
return store.state.appStateModule.satelliteName;
});
function openDropdown(): void {
store.dispatch(APP_STATE_ACTIONS.TOGGLE_ACTIVE_DROPDOWN, APP_STATE_DROPDOWNS.ALL_DASH_ACCOUNT);
}
function closeDropdown(): void {
store.dispatch(APP_STATE_ACTIONS.CLOSE_POPUPS);
}
/**
* Navigates user to billing page.
*/
function navigateToBilling(): void {
closeDropdown();
const routeConf = isNewBillingScreen ?
RouteConfig.Account.with(RouteConfig.Billing).with(RouteConfig.BillingOverview).path :
RouteConfig.Account.with(RouteConfig.Billing).path;
router.push(routeConf);
analytics.pageVisit(routeConf);
}
/**
* Navigates user to account settings page.
*/
function navigateToSettings(): void {
closeDropdown();
analytics.pageVisit(RouteConfig.Account.with(RouteConfig.Settings).path);
router.push(RouteConfig.Account.with(RouteConfig.Settings).path).catch(() => {return;});
}
/**
* Logouts user and navigates to login page.
*/
async function onLogout(): Promise<void> {
analytics.pageVisit(RouteConfig.Login.path);
await router.push(RouteConfig.Login.path);
await Promise.all([
store.dispatch(PM_ACTIONS.CLEAR),
store.dispatch(PROJECTS_ACTIONS.CLEAR),
store.dispatch(USER_ACTIONS.CLEAR),
store.dispatch(ACCESS_GRANTS_ACTIONS.STOP_ACCESS_GRANTS_WEB_WORKER),
store.dispatch(ACCESS_GRANTS_ACTIONS.CLEAR),
store.dispatch(NOTIFICATION_ACTIONS.CLEAR),
store.dispatch(BUCKET_ACTIONS.CLEAR),
store.dispatch(OBJECTS_ACTIONS.CLEAR),
store.dispatch(APP_STATE_ACTIONS.CLEAR),
store.dispatch(PAYMENTS_ACTIONS.CLEAR_PAYMENT_INFO),
store.dispatch(AB_TESTING_ACTIONS.RESET),
store.dispatch('files/clear'),
]);
try {
analytics.eventTriggered(AnalyticsEvent.LOGOUT_CLICKED);
await auth.logout();
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.NAVIGATION_ACCOUNT_AREA);
}
}
</script>
<style scoped lang="scss">
.account-button {
position: relative;
display: flex;
align-items: center;
padding: 14px 18px;
box-sizing: border-box;
color: var(--c-grey-6);
cursor: pointer;
background: var(--c-white);
border: 1px solid var(--c-grey-3);
border-radius: 8px;
height: 44px;
&:hover,
&:active,
&:focus {
border: 1px solid var(--c-blue-3);
}
&__button {
display: flex;
align-items: center;
justify-content: space-evenly;
cursor: pointer;
&__icon {
transition-duration: 0.5s;
margin-right: 10px;
}
&__label {
font-family: 'font_medium', sans-serif;
font-size: 16px;
line-height: 23px;
color: var(--c-grey-6);
margin-right: 10px;
white-space: nowrap;
}
&__label.active {
color: var(--c-blue-3);
}
&__icon.active {
:deep(path) {
fill: var(--c-blue-3);
}
}
}
&__dropdown {
position: absolute;
top: 50px;
right: 0;
background: var(--c-white);
font-family: 'font_regular', sans-serif;
font-style: normal;
font-weight: normal;
width: 270px;
z-index: 999;
cursor: default;
border: 1px solid var(--c-grey-2);
box-sizing: border-box;
box-shadow: 0 -2px 16px rgb(0 0 0 / 10%);
border-radius: 8px;
&__header {
background: var(--c-grey-1);
padding: 16px;
width: calc(100% - 32px);
border: 1px solid var(--c-grey-2);
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 8px 8px 0 0;
&__left,
&__right {
display: flex;
align-items: center;
&__label {
font-size: 14px;
line-height: 20px;
color: var(--c-grey-6);
margin-left: 16px;
}
&__sat {
font-size: 14px;
line-height: 20px;
color: var(--c-grey-6);
margin-right: 16px;
}
&__link {
max-height: 16px;
}
&__link:focus {
svg :deep(path) {
fill: var(--c-blue-3);
}
}
}
}
&__item {
display: flex;
align-items: center;
border-top: 1px solid var(--c-grey-2);
padding: 16px;
width: calc(100% - 32px);
cursor: pointer;
&__label {
margin-left: 16px;
font-size: 14px;
line-height: 20px;
color: var(--c-grey-6);
}
&:last-of-type {
border-radius: 0 0 8px 8px;
}
&:hover {
background-color: var(--c-grey-1);
p {
color: var(--c-blue-3);
}
:deep(path) {
fill: var(--c-blue-3);
}
}
&:focus {
background-color: var(--c-grey-1);
}
}
}
&__arrow {
transition-duration: 0.5s;
}
&__arrow.active {
transform: rotate(180deg) scaleX(-1);
:deep(path) {
fill: var(--c-blue-3);
}
}
&__arrow.hovered {
:deep(path) {
fill: var(--c-blue-3);
}
}
}
</style>

View File

@ -0,0 +1,125 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="my-projects">
<div class="my-projects__header">
<span class="my-projects__header__title">My Projects</span>
<VButton
class="my-projects__header__button"
icon="addcircle"
is-white
:on-press="onCreateProjectClicked"
label="Create a Project"
/>
</div>
<div class="my-projects__list">
<project-item v-for="project in projects" :key="project.id" :project="project" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { Project } from '@/types/projects';
import { useRoute, useRouter, useStore } from '@/utils/hooks';
import { RouteConfig } from '@/router';
import {
AnalyticsEvent,
} from '@/utils/constants/analyticsEventNames';
import { User } from '@/types/users';
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { AnalyticsHttpApi } from '@/api/analytics';
import ProjectItem from '@/views/all-dashboard/components/ProjectItem.vue';
import VButton from '@/components/common/VButton.vue';
const router = useRouter();
const route = useRoute();
const store = useStore();
const analytics = new AnalyticsHttpApi();
/**
* Returns projects list from store.
*/
const projects = computed((): Project[] => {
return store.getters.projects;
});
/**
* Route to create project page.
*/
function onCreateProjectClicked(): void {
analytics.eventTriggered(AnalyticsEvent.CREATE_NEW_CLICKED);
const user: User = store.getters.user;
const ownProjectsCount: number = store.getters.projectsCount;
if (!user.paidTier && user.projectLimit === ownProjectsCount) {
store.commit(APP_STATE_MUTATIONS.UPDATE_ACTIVE_MODAL, MODALS.createProjectPrompt);
} else {
analytics.pageVisit(RouteConfig.CreateProject.path);
store.commit(APP_STATE_MUTATIONS.UPDATE_ACTIVE_MODAL, MODALS.createProject);
}
}
</script>
<style scoped lang="scss">
.my-projects {
&__header {
display: flex;
justify-content: space-between;
align-items: center;
@media screen and (max-width: 425px) {
flex-direction: column;
align-items: start;
gap: 20px;
&__button {
width: 100% !important;
}
}
&__title {
font-size: 24px;
font-family: sans-serif;
font-weight: bold;
line-height: 31px;
}
&__button {
padding: 10px 16px;
border-radius: 8px;
}
}
&__list {
margin-top: 20px;
display: grid;
gap: 10px;
grid-template-columns: 1fr 1fr 1fr 1fr;
& :deep(.project-item) {
overflow: hidden;
}
@media screen and (max-width: 1024px) {
grid-template-columns: 1fr 1fr 1fr;
}
@media screen and (max-width: 786px) {
grid-template-columns: 1fr 1fr;
}
@media screen and (max-width: 425px) {
grid-template-columns: auto;
}
}
}
</style>

View File

@ -0,0 +1,274 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div v-if="project.id" class="project-item">
<div class="project-item__header">
<div class="project-item__header__tag" :class="{member: !isOwner}">
<box-icon />
<span> {{ isOwner ? 'Owner': 'Member' }} </span>
</div>
<a
v-click-outside="closeDropDown" href="" class="project-item__header__menu"
:class="{open: isDropdownOpen}" @click.stop.prevent="toggleDropDown"
>
<menu-icon />
</a>
<div v-if="isDropdownOpen" class="project-item__header__dropdown">
<div class="project-item__header__dropdown__item" @click.stop.prevent="goToProjectEdit">
<gear-icon />
<p class="project-item__header__dropdown__item__label">Project settings</p>
</div>
<div class="project-item__header__dropdown__item" @click.stop.prevent="goToProjectMembers">
<users-icon />
<p class="project-item__header__dropdown__item__label">Invite members</p>
</div>
</div>
</div>
<p class="project-item__name">
{{ project.name }}
</p>
<p class="project-item__description">
{{ project.description }}
</p>
<VButton
class="project-item__button"
width="unset"
height="unset"
:on-press="onOpenClicked"
label="Open Project"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Project } from '@/types/projects';
import { useNotify, useRoute, useRouter, useStore } from '@/utils/hooks';
import {
AnalyticsEvent,
} from '@/utils/constants/analyticsEventNames';
import { User } from '@/types/users';
import { AnalyticsHttpApi } from '@/api/analytics';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { LocalData } from '@/utils/localData';
import { APP_STATE_ACTIONS, PM_ACTIONS } from '@/utils/constants/actionNames';
import { OBJECTS_MUTATIONS } from '@/store/modules/objects';
import { RouteConfig } from '@/router';
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
import { MODALS } from '@/utils/constants/appStatePopUps';
import VButton from '@/components/common/VButton.vue';
import GearIcon from '@/../static/images/common/gearIcon.svg';
import UsersIcon from '@/../static/images/navigation/users.svg';
import MenuIcon from '@/../static/images/allDashboard/menu.svg';
import BoxIcon from '@/../static/images/allDashboard/box.svg';
const router = useRouter();
const route = useRoute();
const store = useStore();
const analytics = new AnalyticsHttpApi();
const notify = useNotify();
const props = withDefaults(defineProps<{
project?: Project,
}>(), {
project: () => new Project(),
});
/**
* isDropdownOpen if dropdown is open.
*/
const isDropdownOpen = computed((): boolean => {
return store.state.appStateModule.appState.activeDropdown === props.project.id;
});
/**
* Returns user entity from store.
*/
const user = computed((): User => {
return store.getters.user;
});
/**
* Returns projects list from store.
*/
const isOwner = computed((): boolean => {
return props.project.ownerId === user.value.id;
});
function toggleDropDown() {
store.dispatch(APP_STATE_ACTIONS.TOGGLE_ACTIVE_DROPDOWN, props.project.id);
}
function closeDropDown() {
store.dispatch(APP_STATE_ACTIONS.CLOSE_POPUPS);
}
/**
* Fetches all project related information.
*/
async function onOpenClicked(): Promise<void> {
await selectProject();
await analytics.eventTriggered(AnalyticsEvent.NAVIGATE_PROJECTS);
store.commit(APP_STATE_MUTATIONS.UPDATE_ACTIVE_MODAL, MODALS.enterPassphrase);
}
async function selectProject() {
await store.dispatch(PROJECTS_ACTIONS.SELECT, props.project.id);
LocalData.setSelectedProjectId(props.project.id);
await store.dispatch(PM_ACTIONS.SET_SEARCH_QUERY, '');
store.commit(OBJECTS_MUTATIONS.CLEAR);
}
/**
* Navigates to project members page.
*/
async function goToProjectMembers(): Promise<void> {
await selectProject();
analytics.pageVisit(RouteConfig.Users.path);
router.push(RouteConfig.Users.path);
closeDropDown();
}
/**
* Fetches all project related information and goes to edit project page.
*/
async function goToProjectEdit(): Promise<void> {
await selectProject();
analytics.pageVisit(RouteConfig.EditProjectDetails.path);
router.push(RouteConfig.EditProjectDetails.path);
closeDropDown();
}
</script>
<style scoped lang="scss">
.project-item {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: flex-start;
padding: 24px;
height: 200px;
background: var(--c-white);
box-shadow: 0 0 20px rgb(0 0 0 / 4%);
border-radius: 8px;
&__header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
&__tag {
display: flex;
justify-content: space-between;
align-items: center;
gap: 5px;
padding: 4px 8px;
border: 1px solid var(--c-purple-2);
border-radius: 24px;
color: var(--c-purple-4);
font-size: 12px;
font-family: 'font_regular', sans-serif;
&.member {
color: var(--c-yellow-5);
border-color: var(--c-yellow-1);
:deep(path) {
fill: var(--c-yellow-5);
}
}
}
&__menu {
width: 24px;
height: 24px;
align-content: center;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
position: relative;
&.open {
background: var(--c-grey-3);
}
}
&__dropdown {
position: absolute;
top: 30px;
right: 0;
background: #fff;
box-shadow: 0 7px 20px rgb(0 0 0 / 15%);
border: 1px solid var(--c-grey-2);
border-radius: 8px;
width: 100%;
z-index: 100;
&__item {
display: flex;
align-items: center;
padding: 15px 25px;
font-family: 'font_regular', sans-serif;
color: var(--c-grey-6);
&__label {
margin: 0 0 0 10px;
}
&:hover {
background-color: var(--c-grey-1);
font-family: 'font_medium', sans-serif;
color: var(--c-blue-3);
svg :deep(path) {
fill: var(--c-blue-3);
}
}
}
}
}
&__name {
margin-top: 16px;
font-weight: bold;
font-family: 'font_regular', sans-serif;
font-size: 24px;
line-height: 31px;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
text-align: start;
}
&__description {
font-weight: 400;
font-size: 14px;
margin-top: 5px;
font-family: 'font_regular', sans-serif;
color: var(--c-grey-6);
line-height: 20px;
}
&__button {
margin-top: 20px;
padding: 10px 16px;
border-radius: 8px;
}
}
</style>

View File

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 887 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="4" viewBox="0 0 16 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0C6.9 0 6 0.9 6 2C6 3.1 6.9 4 8 4C9.1 4 10 3.1 10 2C10 0.9 9.1 0 8 0ZM14 0C12.9 0 12 0.9 12 2C12 3.1 12.9 4 14 4C15.1 4 16 3.1 16 2C16 0.9 15.1 0 14 0ZM2 0C0.9 0 0 0.9 0 2C0 3.1 0.9 4 2 4C3.1 4 4 3.1 4 2C4 0.9 3.1 0 2 0Z" fill="#7C8794"/>
</svg>

After

Width:  |  Height:  |  Size: 352 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.00001 0.700195C10.4794 0.700195 13.3 3.5208 13.3 7.0002C13.3 10.4796 10.4794 13.3002 7.00001 13.3002C3.52062 13.3002 0.700012 10.4796 0.700012 7.0002C0.700012 3.5208 3.52062 0.700195 7.00001 0.700195ZM7.00001 1.8552C4.15851 1.8552 1.85501 4.15869 1.85501 7.0002C1.85501 9.8417 4.15851 12.1452 7.00001 12.1452C9.84152 12.1452 12.145 9.8417 12.145 7.0002C12.145 4.15869 9.84152 1.8552 7.00001 1.8552ZM7.5247 4.82943L7.52494 4.83872L7.52491 6.4563H9.09486C9.41705 6.4563 9.68118 6.71179 9.6919 7.0338C9.70216 7.3423 9.46039 7.60072 9.15188 7.61099L9.14259 7.61122L7.52491 7.6113L7.52501 9.2199C7.52501 9.53884 7.26646 9.7974 6.94751 9.7974C6.63635 9.7974 6.38266 9.5513 6.37047 9.24312L6.37001 9.2199L6.36991 7.6113H4.76141C4.44247 7.6113 4.18391 7.35274 4.18391 7.0338C4.18391 6.72263 4.43001 6.46894 4.73819 6.45675L4.76141 6.4563H6.36991L6.37001 4.88645C6.37001 4.56426 6.62551 4.30012 6.94751 4.28941C7.25602 4.27915 7.51444 4.52092 7.5247 4.82943Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB