deleted frontend code

This commit is contained in:
Čarodej
2022-04-27 08:17:06 +02:00
parent f45c1eb576
commit eb4d5b4cba
142 changed files with 338 additions and 15390 deletions
@@ -1,135 +0,0 @@
<template>
<DatatableWrapper api="/api/admin/dashboard/transactions" :columns="columns" class="overflow-x-auto">
<template slot-scope="{ row }">
<tr class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5">
<td class="py-5 pr-3 md:pr-1">
<span class="text-sm font-bold">
{{ row.data.attributes.note }}
</span>
</td>
<td class="px-3 md:px-1">
<div v-if="row.data.relationships.user" class="flex items-center">
<MemberAvatar :is-border="false" :size="36" :member="row.data.relationships.user" />
<div class="ml-3 pr-10">
<b
class="max-w-1 block overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold"
style="max-width: 155px"
>
{{ row.data.relationships.user.data.attributes.name }}
</b>
<span class="block text-xs text-gray-600 dark:text-gray-500">
{{ row.data.relationships.user.data.attributes.email }}
</span>
</div>
</div>
<span v-if="!row.data.relationships.user" class="text-xs font-bold text-gray-500">
{{ $t('user_was_deleted') }}
</span>
</td>
<td class="px-3 md:px-1">
<ColorLabel
v-if="config.subscriptionType === 'fixed'"
:color="$getTransactionStatusColor(row.data.attributes.status)"
>
{{ $t(row.data.attributes.status) }}
</ColorLabel>
<ColorLabel
v-if="config.subscriptionType === 'metered'"
:color="$getTransactionTypeColor(row.data.attributes.type)"
>
{{ $t(row.data.attributes.type) }}
</ColorLabel>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold" :class="$getTransactionTypeTextColor(row.data.attributes.type)">
{{ $getTransactionMark(row.data.attributes.type) + row.data.attributes.price }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.created_at }}
</span>
</td>
<td class="pl-3 md:pl-1">
<div class="w-28">
<img
class="inline-block max-h-5"
:src="$getPaymentLogo(row.data.attributes.driver)"
:alt="row.data.attributes.driver"
/>
</div>
</td>
</tr>
</template>
<!--Empty page-->
<template v-slot:empty-page>
<InfoBox style="margin-bottom: 0">
<p>{{ $t("not_any_transactions") }}</p>
</InfoBox>
</template>
</DatatableWrapper>
</template>
<script>
import DatatableCellImage from '../../UI/Table/DatatableCellImage'
import DatatableWrapper from '../../UI/Table/DatatableWrapper'
import ColorLabel from '../../UI/Labels/ColorLabel'
import { Trash2Icon, Edit2Icon } from 'vue-feather-icons'
import MemberAvatar from '../../UI/Others/MemberAvatar'
import InfoBox from '../../UI/Others/InfoBox'
import { mapGetters } from 'vuex'
export default {
name: 'WidgetLatestTransactions',
props: ['icon', 'title'],
components: {
DatatableCellImage,
DatatableWrapper,
MemberAvatar,
Trash2Icon,
ColorLabel,
Edit2Icon,
InfoBox,
},
computed: {
...mapGetters(['config']),
},
data() {
return {
columns: [
{
label: this.$t('note'),
field: 'note',
sortable: true,
},
{
label: this.$t('user'),
field: 'user_id',
sortable: true,
},
{
label: this.$t('status'),
field: 'status',
sortable: true,
},
{
label: this.$t('total'),
field: 'amount',
sortable: true,
},
{
label: this.$t('payed_at'),
field: 'created_at',
sortable: true,
},
{
label: this.$t('service'),
field: 'driver',
sortable: true,
},
],
}
},
}
</script>
@@ -144,8 +144,6 @@ export default {
Trash: this.navigation[0].folders,
Public: this.navigation[0].folders,
Files: this.navigation[0].folders,
TeamFolders: this.navigation[1].folders,
SharedWithMe: this.navigation[2].folders,
}[this.$route.name]
},
},
@@ -180,28 +178,11 @@ export default {
},
],
},
{
groupCollapsable: true,
groupTitle: this.$t('collaboration'),
groupLinks: [
{
icon: 'users',
route: 'TeamFolders',
title: this.$t('team_folders'),
},
{
icon: 'user-check',
route: 'SharedWithMe',
title: this.$t('shared_with_me'),
},
],
},
],
}
},
methods: {
resetData() {
this.$store.commit('SET_CURRENT_TEAM_FOLDER', null)
this.$store.commit('CLIPBOARD_CLEAR')
},
goToFolder(folder) {
@@ -48,7 +48,7 @@
@click.native="$shareFileOrFolder(currentFile)"
:title="sharingTitle"
icon="share"
v-if="!$isThisRoute($route, ['Public', 'RequestUpload', 'SharedWithMe'])"
v-if="!$isThisRoute($route, ['Public'])"
/>
<Option
@click.native="$deleteFileOrFolder(currentFile)"
@@ -57,7 +57,7 @@
class="menu-option"
/>
</OptionGroup>
<OptionGroup v-if="!$isThisRoute($route, ['RequestUpload'])">
<OptionGroup>
<Option @click.native="downloadItem" :title="$t('download')" icon="download" />
</OptionGroup>
</PopoverItem>
@@ -79,7 +79,6 @@
<div class="ml-5">
<ToolbarButton
v-if="!$isThisRoute($route, ['RequestUpload'])"
@click.native="downloadItem"
source="download"
:action="$t('download_item')"
@@ -1,202 +0,0 @@
<template>
<div>
<FormLabel icon="wifi">
{{ $te('broadcasting') ? $t('broadcasting') : 'Broadcasting' }}
</FormLabel>
<ValidationProvider
tag="div"
mode="passive"
name="Broadcast Driver"
rules="required"
v-slot="{ errors }"
>
<AppInputText
title="Broadcast Driver"
:error="errors[0]"
:is-last="broadcast.driver === 'none' || broadcast.driver === undefined"
>
<SelectInput
v-model="broadcast.driver"
:options="broadcastDrivers"
placeholder="Select your broadcast driver"
:isError="errors[0]"
/>
</AppInputText>
</ValidationProvider>
<div v-if="broadcast.driver === 'native'">
<ValidationProvider tag="div" mode="passive" name="Host" rules="required" v-slot="{ errors }">
<AppInputText title="Hostname or IP" :error="errors[0]">
<input
class="focus-border-theme input-dark"
v-model="broadcast.host"
placeholder="Type your hostname or IP"
type="text"
:class="{ '!border-rose-600': errors[0] }"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="TLS" v-slot="{ errors }">
<AppInputSwitch
title="Required TLS Connection"
description="When enabled, you must have installed ssl certificate on your server host"
:is-last="true"
>
<SwitchInput v-model="broadcast.tls" :state="broadcast.tls" />
</AppInputSwitch>
</ValidationProvider>
</div>
<div v-if="broadcast.driver === 'pusher'">
<ValidationProvider tag="div" mode="passive" name="App ID" rules="required" v-slot="{ errors }">
<AppInputText title="App ID" :error="errors[0]">
<input
class="focus-border-theme input-dark"
v-model="broadcast.id"
placeholder="Type your app id"
type="text"
:class="{ '!border-rose-600': errors[0] }"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="Key" rules="required" v-slot="{ errors }">
<AppInputText title="Key" :error="errors[0]">
<input
class="focus-border-theme input-dark"
v-model="broadcast.key"
placeholder="Paste your key"
type="text"
:class="{ '!border-rose-600': errors[0] }"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider tag="div" mode="passive" name="Secret" rules="required" v-slot="{ errors }">
<AppInputText title="Secret" :error="errors[0]">
<input
class="focus-border-theme input-dark"
v-model="broadcast.secret"
placeholder="Paste your secret"
type="text"
:class="{ '!border-rose-600': errors[0] }"
/>
</AppInputText>
</ValidationProvider>
<ValidationProvider
tag="div"
mode="passive"
name="Cluster"
rules="required"
v-slot="{ errors }"
>
<AppInputText title="Cluster" :error="errors[0]" :is-last="true">
<SelectInput
v-model="broadcast.cluster"
:options="pusherClusters"
placeholder="Select your cluster"
:isError="errors[0]"
/>
</AppInputText>
</ValidationProvider>
</div>
</div>
</template>
<script>
import {ValidationObserver, ValidationProvider} from 'vee-validate/dist/vee-validate.full'
import SelectInput from '../Inputs/SelectInput'
import AppInputText from './Layouts/AppInputText'
import FormLabel from '../UI/Labels/FormLabel'
import AppInputSwitch from "./Layouts/AppInputSwitch";
import SwitchInput from "../Inputs/SwitchInput";
export default {
name: 'BroadcastSetup',
components: {
SwitchInput,
AppInputSwitch,
ValidationObserver,
ValidationProvider,
AppInputText,
SelectInput,
FormLabel,
},
watch: {
broadcast: {
handler(newValue, oldValue) {
this.$emit('input', newValue)
},
deep: true
},
},
data() {
return {
broadcast: {
driver: undefined,
id: undefined,
key: undefined,
secret: undefined,
cluster: undefined,
tls: true,
},
broadcastDrivers: [
{
label: 'Pusher',
value: 'pusher',
},
{
label: 'VueFileManager',
value: 'native',
},
{
label: 'None',
value: 'none',
},
],
pusherClusters: [
{
label: 'US East (N. Virginia)',
value: 'mt1',
},
{
label: 'Asia Pacific (Singapore)',
value: 'ap1',
},
{
label: 'Asia Pacific (Mumbai)',
value: 'ap2',
},
{
label: 'US East (Ohio)',
value: 'us2',
},
{
label: 'Asia Pacific (Tokyo)',
value: 'ap3',
},
{
label: 'US West (Oregon)',
value: 'us3',
},
{
label: 'Asia Pacific (Sydney)',
value: 'ap4',
},
{
label: 'EU (Ireland)',
value: 'eu',
},
{
label: 'South America (São Paulo)',
value: 'sa1',
},
],
}
}
}
</script>
@@ -1,19 +1,16 @@
<template>
<div>
<VueFolderIcon v-if="!item.data.attributes.isTeamFolder" />
<VueFolderTeamIcon v-if="item.data.attributes.isTeamFolder" style="width: 53px; height: 52px" />
</div>
</template>
<script>
import VueFolderTeamIcon from './VueFolderTeamIcon'
import VueFolderIcon from './VueFolderIcon'
export default {
name: 'FolderIcon',
props: ['item'],
components: {
VueFolderTeamIcon,
VueFolderIcon,
},
}
@@ -1,21 +0,0 @@
<template>
<svg
fill="currentColor"
class="google-icon"
width="22px"
height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M11.0024865,21.9999898 L10.5295935,21.9999898 C4.79941267,21.6719961 0.249801856,17.0571507 0.00497307141,11.324452 C-0.108950196,7.53234629 1.74080914,3.949454 4.89870278,1.84553974 C8.05659642,-0.258374523 12.0763823,-0.586006523 15.5334621,0.978755386 C15.93211,1.16635174 16.2376499,1.50750632 16.3802706,1.9242707 C16.5207552,2.3463989 16.480972,2.80775257 16.2702955,3.19961693 L14.8516162,5.81627558 C14.4628126,6.53476037 13.5910392,6.83955556 12.8390713,6.51991489 C10.9682883,5.78493153 8.83865842,6.25418492 7.45028969,7.70730621 C6.53135645,8.69989447 6.06889516,10.0310827 6.17457812,11.3794238 C6.30701252,12.681964 6.96661172,13.8736694 8.00016536,14.677733 C9.05866753,15.5524314 10.4319771,15.9503782 11.7943075,15.7771694 C12.9539759,15.6214383 13.9992299,14.9976441 14.6866535,14.0510543 L11.4973746,14.0510543 C10.6209566,14.0510751 9.90877945,13.3440121 9.90273519,12.4678659 L9.90273519,9.52137628 C9.90273519,8.64093344 10.6166796,7.92719349 11.4973746,7.92719349 L20.4053605,7.92719349 C21.2793246,7.93309626 21.9880476,8.6367299 22,9.51038192 L22,11.5773224 C21.6954275,17.4249906 16.8597486,22.0079014 11.0024865,21.9999898 Z M11.0024865,2.21012416 C8.62265013,2.2090533 6.34389345,3.17185147 4.68614478,4.87884362 C3.02839611,6.58583578 2.13308042,8.89139591 2.20447576,11.2694802 C2.40041639,15.8515827 6.03742275,19.5398294 10.6175736,19.8011067 C15.4496383,20.0190768 19.5514723,16.2965621 19.8004973,11.4673787 L19.8004973,10.1260663 L12.1022379,10.1260663 L12.1022379,11.8631758 L17.9419175,11.8631758 L17.4910195,13.2924432 C16.7642506,15.7700545 14.6584095,17.5960263 12.1022379,17.9650479 C10.1464891,18.2487094 8.15978033,17.7203963 6.60348115,16.5027975 C5.06921332,15.2995219 4.11012255,13.508076 3.95960166,11.5644046 C3.80908078,9.62073328 4.48093145,7.70305715 5.81166018,6.27803888 C7.69032381,4.25377573 10.5650106,3.48826744 13.2019892,4.3100477 L14.0487978,2.73785364 C13.0730876,2.38128177 12.0413172,2.20254243 11.0024865,2.21012416 Z" id="Shape"></path>
</svg>
</template>
<script>
export default {
name: 'GoogleIcon',
}
</script>
<style lang="scss">
.google-icon path{
color: inherit;
}
</style>
@@ -1,59 +0,0 @@
<template>
<svg
viewBox="0 0 53 39"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g id="V2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="team-folder">
<path
d="M48.03125,6.5 L29.790833,6.5 C28.7431613,6.5 27.7373076,6.08896217 26.9894703,5.35523504 L22.6980297,1.14476496 C21.9501924,0.41103783 20.9443387,-6.36543387e-16 19.896667,0 L4.96875,0 L4.96875,0 C2.22455078,0 0,2.18257812 0,4.875 L0,34.125 C0,36.8174219 2.22455078,39 4.96875,39 L48.03125,39 C50.7754492,39 53,36.8174219 53,34.125 L53,11.375 C53,8.68257813 50.7754492,6.5 48.03125,6.5 Z"
class="svg-color-theme"
stroke="none"
stroke-width="0"
></path>
<path
d="M48.03125,6.5 L29.790833,6.5 C28.7431613,6.5 27.7373076,6.08896217 26.9894703,5.35523504 L22.6980297,1.14476496 C21.9501924,0.41103783 20.9443387,-6.36543387e-16 19.896667,0 L4.96875,0 L4.96875,0 C2.22455078,0 0,2.18257812 0,4.875 L0,34.125 C0,36.8174219 2.22455078,39 4.96875,39 L48.03125,39 C50.7754492,39 53,36.8174219 53,34.125 L53,11.375 C53,8.68257813 50.7754492,6.5 48.03125,6.5 Z"
fill="black"
fill-opacity="0.2"
stroke="none"
stroke-width="0"
></path>
<path
d="M48.03125,12.75 C49.0609313,12.75 49.9941504,13.1577174 50.6692739,13.8201027 C51.3356976,14.4739525 51.75,15.3766531 51.75,16.375 L51.75,16.375 L51.75,34.125 C51.75,35.1233469 51.3356976,36.0260475 50.6692739,36.6798973 C49.9941504,37.3422826 49.0609313,37.75 48.03125,37.75 L48.03125,37.75 L4.96875,37.75 C3.93906868,37.75 3.00584961,37.3422826 2.33072613,36.6798973 C1.66430239,36.0260475 1.25,35.1233469 1.25,34.125 L1.25,34.125 L1.25,16.375 C1.25,15.3766531 1.66430239,14.4739525 2.33072613,13.8201027 C3.00584961,13.1577174 3.93906868,12.75 4.96875,12.75 L4.96875,12.75 Z"
stroke-width="2"
class="svg-color-theme"
fill="green"
></path>
<g
id="Icon"
transform="translate(8.000000, 20.000000)"
class="svg-stroke-theme-darken"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.3"
stroke="black"
stroke-opacity="0.25"
>
<path
d="M9.59999943,10.7999994 L9.59999943,9.59999943 C9.59999943,8.27451611 8.52548289,7.19999957 7.19999957,7.19999957 L2.39999986,7.19999957 C1.07451654,7.19999957 0,8.27451611 0,9.59999943 L0,10.7999994"
></path>
<circle cx="4.79999971" cy="2.39999986" r="2.39999986"></circle>
<path
d="M13.1999992,10.7999994 L13.1999992,9.59999943 C13.1991834,8.50627014 12.4589985,7.55143166 11.3999993,7.27799957"
></path>
<path
d="M8.99999946,0.0779999954 C10.0619483,0.349901852 10.8047053,1.30679461 10.8047053,2.40299986 C10.8047053,3.4992051 10.0619483,4.45609786 8.99999946,4.72799972"
></path>
</g>
</g>
</g>
</svg>
</template>
<script>
export default {
name: 'VueFolderTeamIcon',
}
</script>
@@ -1,83 +0,0 @@
<template>
<div class="page-title left" :class="type">
<h1 class="title" v-html="title"></h1>
<h2 class="description" v-if="description">
{{ description }}
</h2>
</div>
</template>
<script>
export default {
name: 'IndexPageTile',
props: ['title', 'description', 'type'],
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/landing-page';
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.page-title {
position: relative;
z-index: 1;
&.center {
text-align: center;
.title {
margin-left: auto;
margin-right: auto;
max-width: 780px;
}
.description {
margin-left: auto;
margin-right: auto;
}
}
.title {
max-width: 580px;
font-size: 48px;
font-weight: 800;
line-height: 1.3;
margin-bottom: 15px;
/deep/ span {
font-size: 48px;
}
}
.description {
max-width: 580px;
@include font-size(20);
font-weight: 500;
line-height: 1.65;
margin-bottom: 30px;
}
}
@media only screen and (max-width: 960px) {
.page-title {
.title {
max-width: 100%;
font-size: 32px;
line-height: 1.25;
margin-bottom: 15px;
/deep/ span {
font-size: 32px;
}
}
.description {
max-width: 100%;
@include font-size(16);
line-height: 1.6;
margin-bottom: 30px;
}
}
}
</style>
@@ -1,225 +0,0 @@
<template>
<div class="text-center">
<PlanPeriodSwitcher v-if="plans && yearlyPlans.length > 0" v-model="isSelectedYearlyPlans" class="inline-block" />
<div class="plans-wrapper" v-if="plans">
<article class="plan" v-if="plan.data.attributes.interval === intervalPlanType" v-for="plan in plans.data" :key="plan.data.id">
<div class="plan-wrapper">
<header class="plan-header mb-8">
<div class="icon">
<hard-drive-icon class="text-theme mx-auto" size="26" />
</div>
<h1 class="title">{{ plan.data.attributes.name }}</h1>
<h2 class="description">
{{ plan.data.attributes.description }}
</h2>
</header>
<div class="justify-center flex py-1.5" v-for="(value, key, i) in plan.data.attributes.features" :key="i">
<div class="flex items-center">
<CheckIcon size="18" class="svg-stroke-theme mr-2" />
<span class="text-sm font-bold" v-if="value !== -1">
{{ $t( key === 'max_team_members' ? 'max_team_members_total' : key, { value: value }) }}
</span>
<span class="text-sm font-bold" v-if="value === -1">
{{ $t(`${key}.unlimited`) }}
</span>
</div>
</div>
<footer class="plan-footer mt-8">
<b class="price text-theme">
{{ plan.data.attributes.price }} / {{ $t(`interval.${plan.data.attributes.interval}`) }}
</b>
</footer>
</div>
</article>
</div>
</div>
</template>
<script>
import { CheckIcon, HardDriveIcon } from 'vue-feather-icons'
import axios from 'axios'
import PlanPeriodSwitcher from "../../Subscription/PlanPeriodSwitcher";
export default {
name: 'PricingTables',
components: {
PlanPeriodSwitcher,
HardDriveIcon,
CheckIcon,
},
computed: {
intervalPlanType() {
return this.isSelectedYearlyPlans ? 'year' : 'month'
},
yearlyPlans() {
return this.plans.data.filter((plan) => plan.data.attributes.interval === 'year')
},
},
data() {
return {
plans: undefined,
isSelectedYearlyPlans: false,
}
},
created() {
axios.get('api/subscriptions/plans').then((response) => {
this.plans = response.data
this.$emit('load', response.data)
})
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/variables';
@import '../../../../sass/vuefilemanager/mixins';
.plans-wrapper {
box-shadow: 0 7px 20px 5px hsla(220, 36%, 16%, 0.05);
border-radius: 8px;
background: white;
}
.plan {
text-align: center;
flex: 0 0 33%;
padding: 55px 25px 75px;
//border-right: 1px solid #F7F7F7;
&:last-child {
border-right: none;
}
.plan-header {
.icon {
path,
line,
polyline,
rect,
circle {
color: inherit;
}
}
.title {
@include font-size(25);
font-weight: 800;
padding-top: 10px;
}
.description {
@include font-size(14);
font-weight: 600;
}
}
.plan-features {
margin: 55px 0;
.storage-size {
@include font-size(48);
font-weight: 900;
line-height: 1.1;
}
.storage-description {
display: block;
@include font-size(15);
font-weight: 800;
}
}
.plan-footer {
.sign-in-button {
width: 100%;
text-align: center;
}
.price {
@include font-size(18);
display: block;
padding-top: 5px;
.vat-disclaimer {
@include font-size(11);
color: $text;
display: block;
font-weight: 300;
opacity: 0.45;
margin-top: 5px;
}
}
}
}
.plans-wrapper {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
@media only screen and (max-width: 960px) {
.plans-wrapper {
display: block;
margin: 0;
.plan {
padding: 30px 25px;
border-bottom: 1px solid #f7f7f7;
border-right: none;
}
}
}
.dark {
.plans-wrapper {
background: $dark_mode_foreground;
}
.plan {
border-color: $dark_mode_border_color !important;
.plan-wrapper {
background: $dark_mode_foreground;
}
.plan-header {
.title {
color: $dark_mode_text_primary;
}
.description {
color: $dark_mode_text_secondary;
}
}
.plan-features {
.storage-size {
color: $dark_mode_text_primary;
}
.storage-description {
color: $dark_mode_text_primary;
}
}
.plan-footer {
.sign-in-button {
background: rgba($theme, 0.1);
/deep/ .content {
color: $theme;
}
}
.price {
.vat-disclaimer {
color: $dark_mode_text_primary;
}
}
}
}
}
</style>
@@ -1,348 +0,0 @@
<template>
<div class="page-wrapper large get-started" v-if="index.section_get_started === '1'">
<PageTitle
class="page-title"
type="center"
:title="index.get_started_title"
:description="index.get_started_description"
></PageTitle>
<router-link
tag="button"
class="get-started-button bg-theme-800 hover-bg-theme shadow-theme"
:to="{ name: 'SignUp' }"
>
<span class="content">{{ $t('sign_up_now') }}</span>
<chevron-right-icon size="22"></chevron-right-icon>
</router-link>
<cloud-icon size="790" class="cloud-bg svg-color-theme" />
<div class="icons">
<hard-drive-icon size="42" class="icon"></hard-drive-icon>
<settings-icon size="22" class="icon"></settings-icon>
<image-icon size="50" class="icon"></image-icon>
<link-icon size="24" class="icon"></link-icon>
<trash2-icon size="40" class="icon"></trash2-icon>
<search-icon size="18" class="icon"></search-icon>
<eye-icon size="36" class="icon"></eye-icon>
<star-icon size="34" class="icon"></star-icon>
<folder-plus-icon size="20" class="icon"></folder-plus-icon>
<grid-icon size="28" class="icon"></grid-icon>
<share-icon size="32" class="icon"></share-icon>
<folder-plus-icon size="48" class="icon"></folder-plus-icon>
<search-icon size="34" class="icon"></search-icon>
<star-icon size="22" class="icon"></star-icon>
<upload-cloud-icon size="42" class="icon"></upload-cloud-icon>
<grid-icon size="18" class="icon"></grid-icon>
<settings-icon size="32" class="icon"></settings-icon>
<link-icon size="36" class="icon"></link-icon>
<hard-drive-icon size="22" class="icon"></hard-drive-icon>
<info-icon size="36" class="icon"></info-icon>
</div>
</div>
</template>
<script>
import PageTitle from './Components/PageTitle'
import {
ChevronRightIcon,
UploadCloudIcon,
FolderPlusIcon,
HardDriveIcon,
SettingsIcon,
Trash2Icon,
SearchIcon,
ShareIcon,
CloudIcon,
ImageIcon,
InfoIcon,
GridIcon,
LinkIcon,
StarIcon,
EyeIcon,
} from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'IndexGetStarted',
components: {
InfoIcon,
UploadCloudIcon,
ShareIcon,
ChevronRightIcon,
FolderPlusIcon,
HardDriveIcon,
SettingsIcon,
Trash2Icon,
SearchIcon,
CloudIcon,
PageTitle,
ImageIcon,
GridIcon,
LinkIcon,
StarIcon,
EyeIcon,
},
computed: {
...mapGetters(['index']),
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/landing-page';
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
.icons {
.icon {
position: absolute;
&:nth-child(20) {
bottom: -37%;
left: 37%;
transform: rotate(0deg);
circle,
line {
stroke: $yellow;
}
}
&:nth-child(19) {
bottom: -21%;
left: 23.5%;
transform: rotate(-20deg);
path,
line {
stroke: $purple;
}
}
&:nth-child(18) {
bottom: -4%;
left: 26.5%;
transform: rotate(0deg);
path {
stroke: $theme;
}
}
&:nth-child(17) {
bottom: -5%;
left: 8.5%;
transform: rotate(0deg);
}
&:nth-child(16) {
top: 86%;
left: 17%;
transform: rotate(18deg);
}
&:nth-child(15) {
top: 64%;
left: 17%;
transform: rotate(0deg);
polyline,
line,
path {
stroke: $red;
}
}
&:nth-child(14) {
top: 44%;
left: 28%;
transform: rotate(0deg);
polygon {
stroke: $purple;
}
}
&:nth-child(13) {
top: 33%;
left: 16%;
transform: rotate(0deg);
}
&:nth-child(12) {
top: 23%;
left: 32%;
transform: rotate(13deg);
line,
path {
stroke: $yellow;
}
}
&:nth-child(1) {
top: 35%;
right: 49%;
transform: rotate(-11deg);
line,
path {
stroke: $theme;
}
}
&:nth-child(2) {
top: 12%;
right: 45%;
transform: rotate(0);
circle,
path {
stroke: $red;
}
}
&:nth-child(3) {
top: 30%;
right: 30%;
transform: rotate(20deg);
}
&:nth-child(4) {
top: 14%;
right: 14.5%;
transform: rotate(-1deg);
}
&:nth-child(5) {
top: 62%;
right: 15.5%;
transform: rotate(21deg);
polyline,
path,
line {
stroke: $red;
}
}
&:nth-child(6) {
top: 66%;
right: 26.5%;
transform: rotate(0deg);
}
&:nth-child(7) {
bottom: 3%;
right: 21.5%;
transform: rotate(16deg);
}
&:nth-child(8) {
bottom: -13%;
right: 16.5%;
transform: rotate(0deg);
polygon {
stroke: $yellow;
}
}
&:nth-child(9) {
bottom: -32%;
right: 27%;
transform: rotate(-20deg);
}
&:nth-child(10) {
bottom: -5%;
right: 34%;
transform: rotate(16deg);
rect {
stroke: $purple;
}
}
&:nth-child(11) {
bottom: -28%;
right: 44%;
transform: rotate(-12deg);
polyline,
line,
path {
stroke: $red;
}
}
}
}
.cloud-bg {
z-index: 0;
position: absolute;
top: 70px;
right: 60px;
transform: scale(-1, 1) rotate(13deg);
opacity: 0.1;
path {
stroke: none;
}
}
.page-title {
padding-top: 340px;
}
.get-started-button {
display: flex;
align-items: center;
outline: none;
border: none;
margin-left: auto;
margin-right: auto;
cursor: pointer;
padding: 20px 36px;
border-radius: 6px;
//box-shadow: 0 5px 10px 2px rgba($theme, 0.34);
margin-bottom: 395px;
@include transition(150ms);
position: relative;
z-index: 1;
.content {
@include font-size(19);
font-weight: 700;
margin-right: 8px;
color: white;
}
polyline {
stroke: white;
}
}
@media only screen and (max-width: 1190px) {
.get-started-button {
margin-bottom: 280px;
}
}
@media only screen and (max-width: 960px) {
.page-title {
padding-top: 20px;
}
.get-started-button {
margin-bottom: 30px;
}
.cloud-bg,
.icons {
display: none;
}
}
</style>
@@ -1,227 +0,0 @@
<template>
<div class="page-wrapper large hero-screenshot">
<img class="hero-light" src="/assets/images/vuefilemanager-screenshot-light.png" :alt="config.app_name" />
<img class="hero-dark" src="/assets/images/vuefilemanager-screenshot-dark.png" :alt="config.app_name" />
<div class="icons">
<link-icon size="20" class="icon"></link-icon>
<image-icon size="38" class="icon"></image-icon>
<hard-drive-icon size="34" class="icon"></hard-drive-icon>
<folder-plus-icon size="40" class="icon"></folder-plus-icon>
<settings-icon size="18" class="icon"></settings-icon>
<search-icon size="24" class="icon"></search-icon>
<star-icon size="18" class="icon"></star-icon>
<trash2-icon size="32" class="icon"></trash2-icon>
<search-icon size="18" class="icon"></search-icon>
<eye-icon size="30" class="icon"></eye-icon>
<star-icon size="30" class="icon"></star-icon>
<folder-plus-icon size="16" class="icon"></folder-plus-icon>
<grid-icon size="20" class="icon"></grid-icon>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import {
FolderPlusIcon,
HardDriveIcon,
SettingsIcon,
Trash2Icon,
SearchIcon,
ImageIcon,
GridIcon,
LinkIcon,
StarIcon,
EyeIcon,
} from 'vue-feather-icons'
export default {
name: 'IndexHeroScreenshot',
components: {
FolderPlusIcon,
HardDriveIcon,
SettingsIcon,
Trash2Icon,
SearchIcon,
ImageIcon,
GridIcon,
LinkIcon,
StarIcon,
EyeIcon,
},
computed: {
...mapGetters(['config']),
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/landing-page';
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
.icons {
.icon {
z-index: 0;
position: absolute;
&:nth-child(1) {
top: -14%;
right: 2%;
}
&:nth-child(2) {
top: -5%;
right: 14%;
transform: rotate(19deg);
}
&:nth-child(3) {
top: -6.5%;
right: 28.5%;
transform: rotate(-12deg);
line,
path {
stroke: $theme;
}
}
&:nth-child(4) {
top: -9.5%;
right: 41.5%;
transform: rotate(13deg);
path,
line {
stroke: $yellow;
}
}
&:nth-child(5) {
top: -16%;
right: 26%;
circle,
path {
stroke: $red;
}
}
&:nth-child(6) {
top: -13%;
right: 49%;
}
&:nth-child(7) {
top: 2.5%;
right: 46%;
polygon {
stroke: $purple;
}
}
&:nth-child(8) {
top: 13%;
right: 2.5%;
transform: rotate(22deg);
polyline,
path,
line {
stroke: $red;
}
}
&:nth-child(9) {
top: 14%;
right: 11%;
circle,
line {
stroke: $purple;
}
}
&:nth-child(10) {
top: 29%;
right: 7%;
transform: rotate(19deg);
}
&:nth-child(11) {
top: 38%;
right: 3%;
polygon {
stroke: $yellow;
}
}
&:nth-child(12) {
top: 50%;
right: 11.5%;
transform: rotate(-22deg);
}
&:nth-child(13) {
top: 34%;
right: 16%;
transform: rotate(13deg);
rect {
stroke: $theme;
}
}
}
}
.hero-screenshot {
position: relative;
z-index: 1;
padding-top: 75px;
padding-bottom: 130px;
img {
border-radius: 8px;
width: 80%;
box-shadow: 0 7px 255px rgba(#19363c, 0.1);
&.hero-dark {
display: none;
}
}
}
@media only screen and (max-width: 890px) {
.icons {
display: none;
}
.hero-screenshot {
padding-top: 40px;
padding-bottom: 90px;
img {
width: 100%;
}
}
}
.dark {
.hero-screenshot {
img {
&.hero-light {
display: none;
}
&.hero-dark {
display: block;
box-shadow: 0 7px 185px rgba(0, 0, 0, 0.8);
}
}
}
}
</style>
@@ -1,198 +0,0 @@
<template>
<section class="main-features page-wrapper medium">
<PageTitle
v-if="index.section_features === '1'"
type="center"
:title="index.features_title"
:description="index.features_description"
></PageTitle>
<div v-if="index.section_feature_boxes === '1'" class="content">
<div class="hero">
<img src="/assets/images/hero-Illustration.svg" alt="Hero" />
</div>
<div class="features">
<div class="feature">
<div class="icon">
<cloud-icon size="24"></cloud-icon>
</div>
<h3 class="title">
{{ index.feature_title_1 }}
</h3>
<p class="description">
{{ index.feature_description_1 }}
</p>
</div>
<div class="feature">
<div class="icon">
<user-icon size="24"></user-icon>
</div>
<h3 class="title">
{{ index.feature_title_2 }}
</h3>
<p class="description">
{{ index.feature_description_2 }}
</p>
</div>
<div class="feature">
<div class="icon">
<hard-drive-icon size="24"></hard-drive-icon>
</div>
<h3 class="title">
{{ index.feature_title_3 }}
</h3>
<p class="description">
{{ index.feature_description_3 }}
</p>
</div>
</div>
</div>
</section>
</template>
<script>
import { UserIcon, CloudIcon, HardDriveIcon } from 'vue-feather-icons'
import PageTitle from './Components/PageTitle'
import { mapGetters } from 'vuex'
export default {
name: 'IndexMainFeatures',
components: {
PageTitle,
HardDriveIcon,
CloudIcon,
UserIcon,
},
computed: {
...mapGetters(['index']),
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/landing-page';
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
.features {
padding-left: 75px;
.feature {
margin-bottom: 25px;
.title {
@include font-size(26);
font-weight: 800;
margin-bottom: 4px;
}
.description {
line-height: 1.5;
color: $text-muted;
@include font-size(18);
}
.icon {
border-radius: 12px;
width: 44px;
height: 44px;
display: flex;
align-items: center;
margin-bottom: 18px;
svg {
margin: 0 auto;
}
}
&:nth-child(1) .icon {
background: rgba($yellow, 0.1);
path,
line,
polyline,
rect,
circle {
stroke: $yellow;
}
}
&:nth-child(2) .icon {
background: rgba($theme, 0.1);
path,
line,
polyline,
rect,
circle {
stroke: $theme;
}
}
&:nth-child(3) .icon {
background: rgba($purple, 0.1);
path,
line,
polyline,
rect,
circle {
stroke: $purple;
}
}
}
}
.content {
margin-top: 107px;
display: flex;
}
@media only screen and (max-width: 1190px) {
.hero {
flex: 0 0 60%;
img {
width: 100%;
}
}
.features {
padding-left: 25px;
margin-top: 0px;
}
}
@media only screen and (max-width: 960px) {
.content {
display: block;
margin-top: 40px;
}
.features {
margin-top: 50px;
padding-left: 0;
.feature {
margin-bottom: 35px;
.title {
@include font-size(22);
}
.description {
@include font-size(16);
}
}
}
}
.dark {
.features {
.feature {
.description {
color: $dark_mode_text_secondary;
}
}
}
}
</style>
@@ -1,140 +0,0 @@
<template>
<nav class="main-navigation">
<router-link :to="{ name: 'Homepage' }" tag="div" class="logo">
<img
class="max-h-6"
v-if="config.app_logo_horizontal"
:src="$getImage(logoSrc)"
:alt="config.app_name"
/>
<b v-if="!config.app_logo_horizontal" class="logo-text">{{ config.app_name }}</b>
</router-link>
<div class="navigation">
<ul class="navigation-links">
<!--<li v-if="config.stripe_public_key">
<a href="/#pricing">
{{ $t('pricing') }}
</a>
</li>-->
<li>
<router-link :to="{ name: 'ContactUs' }" class="hover-text-theme">
{{ $t('contact_us') }}
</router-link>
</li>
</ul>
<ul v-if="!config.isAuthenticated" class="navigation-links">
<li>
<router-link :to="{ name: 'SignIn' }" class="hover-text-theme">
{{ $t('log_in') }}
</router-link>
</li>
<li v-if="config.userRegistration">
<router-link class="cta-button text-theme bg-theme-100" :to="{ name: 'SignUp' }">
{{ $t('page_index.menu.sign_in') }}
</router-link>
</li>
</ul>
<ul v-if="config.isAuthenticated" class="navigation-links">
<li>
<router-link class="cta-button text-theme bg-theme-100" :to="{ name: 'Files' }">
{{ $t('go_to_files') }}
</router-link>
</li>
</ul>
</div>
<router-link class="cta-button log-in text-theme bg-theme-100" :to="{ name: 'SignIn' }">
{{ $t('log_in') }}
</router-link>
</nav>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'IndexNavigation',
computed: {
...mapGetters(['config', 'index', 'isDarkMode']),
logoSrc() {
return this.isDarkMode && this.config.app_logo_horizontal ? this.config.app_logo_horizontal_dark : this.config.app_logo_horizontal
}
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/landing-page';
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
.main-navigation {
justify-content: space-between;
padding-bottom: 25px;
align-items: center;
padding-top: 25px;
display: flex;
}
.logo {
cursor: pointer;
img {
cursor: pointer;
height: 38px;
width: auto;
}
.logo-text {
font-weight: 800;
@include font-size(25);
}
}
.navigation-links {
display: inline-block;
margin-left: 25px;
&:first-child {
margin-left: 0;
}
li {
display: inline-block;
a {
padding: 14px;
font-weight: 700;
@include font-size(17);
@include transition(150ms);
}
}
}
.cta-button {
border-radius: 6px;
padding: 8px 23px;
@include font-size(17);
font-weight: 700;
&.log-in {
display: none;
}
}
@media only screen and (max-width: 690px) {
.navigation {
display: none;
}
.logo {
img {
height: auto;
width: 190px;
}
}
.cta-button.log-in {
display: block;
}
}
</style>
@@ -1,120 +0,0 @@
<template>
<footer class="page-wrapper medium">
<router-link :to="{ name: 'Homepage' }" tag="div" class="logo">
<img
v-if="config.app_logo_horizontal"
:src="$getImage(logoSrc)"
:alt="config.app_name"
class="mx-auto max-h-6"
/>
<b v-if="!config.app_logo_horizontal" class="logo-text">{{ config.app_name }}</b>
</router-link>
<ul class="navigation-links">
<!-- <li>
<a href="/#pricing">
{{ $t('pricing') }}
</a>
</li>-->
<li>
<router-link :to="{ name: 'ContactUs' }" class="hover-text-theme">
{{ $t('contact_us') }}
</router-link>
</li>
</ul>
<ul class="navigation-links">
<li v-if="legal.visibility" v-for="(legal, index) in config.legal" :key="index">
<router-link :to="{ name: 'DynamicPage', params: { slug: legal.slug } }" class="hover-text-theme">
{{ legal.title }}
</router-link>
</li>
</ul>
<p class="copyright" v-html="config.app_footer"></p>
</footer>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'IndexPageFooter',
computed: {
...mapGetters(['config', 'isDarkMode']),
logoSrc() {
return this.isDarkMode && this.config.app_logo_horizontal ? this.config.app_logo_horizontal_dark : this.config.app_logo_horizontal
},
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/landing-page';
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
footer {
text-align: center;
padding-top: 80px;
}
.logo {
margin-bottom: 15px;
cursor: pointer;
img {
height: 38px;
width: auto;
}
.logo-text {
font-weight: 800;
@include font-size(25);
}
}
.navigation-links {
display: inline-block;
li {
display: inline-block;
a {
display: block;
padding: 19px;
font-weight: 700;
@include font-size(17);
@include transition(150ms);
}
}
}
.copyright {
@include font-size(17);
color: $text-muted;
padding-top: 50px;
padding-bottom: 20px;
/deep/ a {
font-weight: 700;
}
}
@media only screen and (max-width: 960px) {
.navigation-links {
display: block;
li {
display: block;
a {
padding: 10px 0;
}
}
}
}
.dark {
.copyright {
color: $dark_mode_text_secondary;
}
}
</style>
@@ -1,139 +0,0 @@
<template>
<header class="main-header page-wrapper medium">
<PageTitle :title="index.header_title" :description="index.header_description" />
<div v-if="!config.isAuthenticated">
<!--User registration button-->
<router-link v-if="config.userRegistration" class="sign-up-button" :to="{ name: 'SignUp' }">
<AuthButton class="button" icon="chevron-right" :text="$t('page_index.sign_up_button')" />
</router-link>
<!--User login button-->
<router-link v-if="!config.userRegistration" class="sign-up-button" :to="{ name: 'SignIn' }">
<AuthButton class="button" icon="chevron-right" :text="$t('log_in')" />
</router-link>
<div class="features" v-if="config.subscriptionType === 'fixed'">
<div class="feature">
<credit-card-icon size="19" class="feature-icon"></credit-card-icon>
<b class="feature-title">{{ $t('page_index.sign_feature_1') }}</b>
</div>
<div class="feature">
<hard-drive-icon size="19" class="feature-icon"></hard-drive-icon>
<b class="feature-title">{{
$t('page_index.sign_feature_2', {
defaultSpace: config.storageDefaultSpaceFormatted,
})
}}</b>
</div>
</div>
</div>
</header>
</template>
<script>
import HardDriveIcon from 'vue-feather-icons/icons/HardDriveIcon'
import PageTitle from './Components/PageTitle'
import AuthButton from '../UI/Buttons/AuthButton'
import { CreditCardIcon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'IndexPageHeader',
components: {
PageTitle,
CreditCardIcon,
HardDriveIcon,
AuthButton,
},
computed: {
...mapGetters(['index', 'config']),
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/landing-page';
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
.features {
display: flex;
margin-top: 35px;
.feature {
display: flex;
margin-right: 35px;
&:nth-child(1) {
path,
line,
polyline,
rect,
circle {
stroke: $yellow;
}
}
&:nth-child(2) {
path,
line,
polyline,
rect,
circle {
stroke: $purple;
}
}
&:last-child {
margin-right: 0;
}
.feature-title {
@include font-size(14);
font-weight: 700;
}
.feature-icon {
margin-right: 10px;
}
}
}
.main-header {
padding-top: 70px;
}
.sign-up-button {
margin-top: 65px;
display: block;
.button {
margin-left: 0;
margin-right: 0;
}
}
@media only screen and (max-width: 690px) {
.main-header {
padding-top: 50px;
}
.features {
display: block;
.feature {
margin-right: 0;
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
}
}
.sign-up-button {
margin-top: 30px;
}
}
</style>
@@ -1,176 +0,0 @@
<template>
<div
class="page-wrapper medium pricing"
v-if="!isEmpty && index.section_pricing_content === '1' && config.stripe_public_key"
>
<div id="pricing" class="page-title center">
<h1 class="title" v-html="index.pricing_title"></h1>
</div>
<PricingTables class="pricing-tables" @load="pricingLoaded" />
<div class="page-title center">
<h2 class="description">
{{ index.pricing_description }}
</h2>
<router-link class="sign-up-button" :to="{ name: 'SignUp' }">
<AuthButton class="button" icon="chevron-right" :text="$t('page_index.sign_up_button')" />
</router-link>
</div>
<cloud-icon size="800" class="cloud-bg"></cloud-icon>
<cloud-icon size="560" class="cloud-bg"></cloud-icon>
</div>
</template>
<script>
import PricingTables from './Components/PricingTables'
import AuthButton from '../UI/Buttons/AuthButton'
import { CloudIcon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'IndexPricingTables',
components: {
PricingTables,
AuthButton,
CloudIcon,
},
computed: {
...mapGetters(['index', 'config']),
},
data() {
return {
isEmpty: false,
}
},
methods: {
pricingLoaded(pricing) {
if (pricing.length === 0) this.isEmpty = true
},
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/landing-page';
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
.pricing {
.cloud-bg {
z-index: 0;
path {
stroke: none;
fill: rgba($theme, 0.05);
}
&:first-of-type {
position: absolute;
top: 30px;
right: -130px;
transform: scale(-1, 1) rotate(-17deg);
}
&:last-of-type {
position: absolute;
bottom: 160px;
left: -230px;
transform: rotate(13deg);
}
}
}
.page-title {
position: relative;
z-index: 1;
&.center {
text-align: center;
.title {
margin-left: auto;
margin-right: auto;
}
.description {
margin-left: auto;
margin-right: auto;
}
}
.title {
max-width: 580px;
font-size: 48px;
font-weight: 800;
line-height: 1.25;
margin-bottom: 15px;
/deep/ span {
font-size: 48px;
}
}
.description {
max-width: 580px;
@include font-size(20);
font-weight: 500;
line-height: 1.6;
margin-bottom: 30px;
}
}
.pricing {
padding-top: 250px;
padding-bottom: 120px;
}
.pricing-tables {
margin-top: 50px;
margin-bottom: 60px;
position: relative;
z-index: 1;
}
.sign-up-button {
padding-top: 10px;
display: block;
}
@media only screen and (max-width: 1190px) {
.cloud-bg {
display: none;
}
.pricing {
padding-top: 150px;
padding-bottom: 60px;
}
}
@media only screen and (max-width: 960px) {
.page-title {
.title {
font-size: 28px;
line-height: 1.25;
margin-bottom: 15px;
/deep/ span {
font-size: 28px;
}
}
.description {
@include font-size(16);
line-height: 1.6;
margin-bottom: 30px;
}
}
.pricing {
padding-top: 50px;
padding-bottom: 120px;
}
}
</style>
@@ -43,7 +43,7 @@
<ListInfoItem :title="$t('created_at')" :content="singleFile.data.attributes.created_at" />
<!--Location-->
<ListInfoItem v-if="$checkPermission(['master']) && !isTeamsHomepage" :title="$t('where')">
<ListInfoItem v-if="$checkPermission(['master'])" :title="$t('where')">
<div @click="$moveFileOrFolder(singleFile)" class="flex cursor-pointer items-center">
<b class="inline-block text-sm font-bold">
{{
@@ -84,16 +84,6 @@
</div>
</ListInfoItem>
<!--Author-->
<ListInfoItem v-if="canShowAuthor" :title="$t('author')">
<div class="mt-1.5 flex items-center">
<MemberAvatar :size="32" :member="singleFile.data.relationships.creator" />
<span class="ml-3 block text-sm font-bold">
{{ singleFile.data.relationships.creator.data.attributes.name }}
</span>
</div>
</ListInfoItem>
<!--Metadata-->
<ListInfoItem v-if="canShowMetaData" :title="$t('meta_data')">
<ImageMetaData />
@@ -108,7 +98,6 @@ import CopyShareLink from '../../Inputs/CopyShareLink'
import {Edit2Icon, LockIcon, UnlockIcon, EyeOffIcon} from 'vue-feather-icons'
import ImageMetaData from '../../UI/Others/ImageMetaData'
import TitlePreview from '../../UI/Labels/TitlePreview'
import TeamMembersPreview from '../../Teams/Components/TeamMembersPreview'
import ListInfoItem from '../../UI/List/ListInfoItem'
import MemberAvatar from '../../UI/Others/MemberAvatar'
import {mapGetters} from 'vuex'
@@ -116,7 +105,6 @@ import {mapGetters} from 'vuex'
export default {
name: 'InfoSidebar',
components: {
TeamMembersPreview,
FilePreviewDetail,
ImageMetaData,
CopyShareLink,
@@ -156,17 +144,6 @@ export default {
return title ? this.$t(title.label) : this.$t('can_download_file')
},
canShowAuthor() {
return (
this.$isThisRoute(this.$route, ['SharedWithMe', 'TeamFolders'])
&& this.clipboard[0].data.type !== 'folder'
&& this.clipboard[0].data.relationships.creator
&& this.user.data.id !== this.clipboard[0].data.relationships.creator.data.id
)
},
isTeamsHomepage() {
return this.$isThisRoute(this.$route, ['TeamFolders', 'SharedWithMe']) && !this.$route.params.id
},
},
}
</script>
@@ -1,133 +0,0 @@
<template>
<div
class="hidden w-[300px] shrink-0 overflow-y-auto overflow-x-hidden px-2.5 pt-2 lg:block xl:w-[320px] 2xl:w-[360px]"
>
<!--Is empty clipboard-->
<div v-if="isEmpty" class="flex h-full items-center justify-center">
<div class="text-center">
<eye-off-icon size="22" class="vue-feather mb-3 inline-block text-gray-500" />
<small class="block text-xs text-gray-500">
{{ $t('nothing_to_preview') }}
</small>
</div>
</div>
<!--Multiple item selection-->
<TitlePreview
v-if="!isSingleFile && !isEmpty"
class="mb-6"
icon="check-square"
:title="$t('selected_multiple')"
:subtitle="this.clipboard.length + ' ' + $tc('items', this.clipboard.length)"
/>
<!--Single file preview-->
<div v-if="isSingleFile && !isEmpty">
<FilePreviewDetail />
<TitlePreview
class="mb-6"
:icon="clipboard[0].data.type"
:title="clipboard[0].data.attributes.name"
:subtitle="clipboard[0].data.attributes.mimetype"
/>
<!--Filesize-->
<ListInfoItem
v-if="singleFile.data.attributes.filesize"
:title="$t('size')"
:content="singleFile.data.attributes.filesize"
/>
<!--Created At-->
<ListInfoItem :title="$t('created_at')" :content="singleFile.data.attributes.created_at" />
<!--Location-->
<ListInfoItem :title="$t('where')">
<div @click="$moveFileOrFolder(singleFile)" class="flex cursor-pointer items-center">
<b class="inline-block text-sm font-bold">
{{
singleFile.data.relationships.parent
? singleFile.data.relationships.parent.data.attributes.name
: $getCurrentLocationName()
}}
</b>
<Edit2Icon size="10" class="ml-2" />
</div>
</ListInfoItem>
<!--Shared-->
<ListInfoItem
v-if="$checkPermission('master') && singleFile.data.relationships.shared"
:title="$t('shared')"
>
<div @click="$shareFileOrFolder(singleFile)" class="mb-2 flex cursor-pointer items-center">
<span class="inline-block text-sm font-bold">
{{ sharedInfo }}
</span>
<Edit2Icon size="10" class="ml-2" />
</div>
<div class="flex w-full items-center">
<lock-icon
v-if="isLocked"
@click="$shareFileOrFolder(singleFile)"
size="17"
class="hover-text-theme vue-feather cursor-pointer"
/>
<unlock-icon
v-if="!isLocked"
@click="$shareFileOrFolder(singleFile)"
size="17"
class="hover-text-theme vue-feather cursor-pointer"
/>
<CopyShareLink :item="singleFile" size="small" class="w-full pl-2.5" />
</div>
</ListInfoItem>
<!--Metadata-->
<ListInfoItem v-if="canShowMetaData" :title="$t('meta_data')">
<ImageMetaData />
</ListInfoItem>
</div>
</div>
</template>
<script>
import { Edit2Icon, LockIcon, UnlockIcon, EyeOffIcon } from 'vue-feather-icons'
import FilePreviewDetail from '../../Others/FilePreviewDetail'
import ListInfoItem from '../../UI/List/ListInfoItem'
import ImageMetaData from '../../UI/Others/ImageMetaData'
import TitlePreview from '../../UI/Labels/TitlePreview'
import { mapGetters } from 'vuex'
export default {
name: 'InfoSidebarUploadRequest',
components: {
FilePreviewDetail,
ImageMetaData,
TitlePreview,
ListInfoItem,
UnlockIcon,
EyeOffIcon,
Edit2Icon,
LockIcon,
},
computed: {
...mapGetters(['clipboard']),
isEmpty() {
return this.clipboard.length === 0
},
isSingleFile() {
return this.clipboard.length === 1
},
singleFile() {
return this.clipboard[0]
},
canShowMetaData() {
return (
this.clipboard[0].data.attributes.metadata && this.clipboard[0].data.attributes.metadata.ExifImageWidth
)
},
},
}
</script>
@@ -13,48 +13,30 @@
/>
<PopoverItem name="desktop-create" side="left">
<OptionGroup
:title="$t('frequently_used')"
:title="$t('create')"
>
<OptionUpload
:title="$t('upload_files')"
type="file"
:class="{
'is-inactive': (isSharedWithMe && !canEdit) || canUploadInView || isTeamFolderHomepage || isSharedWithMeHomepage,
'is-inactive': canUploadInView,
}"
/>
<Option
@click.native="$createFolder"
:class="{
'is-inactive': (isSharedWithMe && !canEdit) || canCreateFolder || isTeamFolderHomepage || isSharedWithMeHomepage,
}"
:title="$t('create_folder')"
icon="folder-plus"
/>
</OptionGroup>
<OptionGroup :title="$t('others')">
<OptionUpload
:class="{
'is-inactive': (isSharedWithMe && !canEdit) || canUploadFolderInView || isTeamFolderHomepage || isSharedWithMeHomepage,
'is-inactive': canUploadFolderInView,
}"
:title="$t('upload_folder')"
type="folder"
/>
<Option
@click.stop.native="$openRemoteUploadPopup"
:title="$t('remote_upload')"
icon="remote-upload"
/>
<Option
@click.stop.native="$createTeamFolder"
:class="{ 'is-inactive': canCreateTeamFolder }"
:title="$t('create_team_folder')"
icon="users"
@click.native="$createFolder"
:class="{
'is-inactive': canCreateFolder,
}"
:title="$t('create_folder')"
icon="folder-plus"
/>
<Option
@click.native="$createFileRequest"
:title="$t('create_file_request')"
icon="upload-cloud"
/>
</OptionGroup>
</PopoverItem>
</PopoverWrapper>
@@ -64,53 +46,11 @@
<!--File Controls-->
<div class="ml-5 flex items-center xl:ml-8">
<!--Team Heads-->
<PopoverWrapper v-if="$isThisRoute($route, ['TeamFolders', 'SharedWithMe'])">
<TeamMembersButton
@click.stop.native="showTeamFolderMenu"
size="32"
class="cursor-pointer rounded-lg py-0.5 pl-2 pr-0.5 hover:bg-light-background dark:hover:bg-dark-foreground"
/>
<PopoverItem name="team-folder" side="left">
<TeamFolderPreview />
<OptionGroup v-if="$isThisRoute($route, ['TeamFolders'])" :title="$t('options')">
<Option
@click.native="$updateTeamFolder(teamFolder)"
:title="$t('edit_members')"
icon="rename"
/>
<Option
@click.native="$dissolveTeamFolder(teamFolder)"
:title="$t('dissolve_team')"
icon="trash"
/>
</OptionGroup>
<OptionGroup v-if="$isThisRoute($route, ['SharedWithMe'])" :title="$t('options')">
<Option
@click.native="$detachMeFromTeamFolder(teamFolder)"
:title="$t('leave_team_folder')"
icon="user-minus"
/>
</OptionGroup>
</PopoverItem>
</PopoverWrapper>
<!--Action buttons-->
<div v-if="!$isMobile()" class="flex items-center">
<ToolbarButton
v-if="canShowConvertToTeamFolder"
@click.native="$convertAsTeamFolder(clipboard[0])"
:class="{
'is-inactive': !canCreateTeamFolder,
}"
source="user-plus"
:action="$t('convert_into_team_folder')"
/>
<ToolbarButton
v-if="!$isThisRoute($route, ['SharedWithMe', 'Public'])"
v-if="!$isThisRoute($route, ['Public'])"
@click.native="$shareFileOrFolder(clipboard[0])"
:class="{
'is-inactive': canShareInView,
@@ -121,7 +61,7 @@
<ToolbarButton
@click.native="$moveFileOrFolder(clipboard[0])"
:class="{
'is-inactive': canMoveInView && !canEdit,
'is-inactive': canMoveInView,
}"
source="move"
:action="$t('move')"
@@ -129,7 +69,7 @@
<ToolbarButton
@click.native="$deleteFileOrFolder(clipboard[0])"
:class="{
'is-inactive': canDeleteInView && !canEdit,
'is-inactive': canDeleteInView,
}"
source="trash"
:action="$t('delete')"
@@ -163,8 +103,6 @@
</template>
<script>
import TeamMembersButton from '../../Teams/Components/TeamMembersButton'
import TeamFolderPreview from '../../Teams/Components/TeamFolderPreview'
import PopoverWrapper from '../../UI/Popover/PopoverWrapper'
import FileSortingOptions from '../../Menus/FileSortingOptions'
import PopoverItem from '../../UI/Popover/PopoverItem'
@@ -182,8 +120,6 @@ export default {
name: 'DesktopToolbar',
components: {
FileSortingOptions,
TeamMembersButton,
TeamFolderPreview,
SearchBarButton,
UploadProgress,
PopoverWrapper,
@@ -197,71 +133,34 @@ export default {
computed: {
...mapGetters([
'isVisibleNavigationBars',
'currentTeamFolder',
'currentFolder',
'sharedDetail',
'clipboard',
'user',
]),
canEdit() {
if (this.currentTeamFolder && this.user) {
let member = this.currentTeamFolder.data.relationships.members.data.find(
(member) => member.data.id === this.user.data.id
)
return member.data.attributes.permission === 'can-edit'
}
return false
},
teamFolder() {
return this.currentTeamFolder ? this.currentTeamFolder : this.clipboard[0]
},
isTeamFolderHomepage() {
return this.$isThisRoute(this.$route, ['TeamFolders']) && !this.$route.params.id
},
isSharedWithMe() {
return this.$isThisRoute(this.$route, ['SharedWithMe'])
},
isSharedWithMeHomepage() {
return this.$isThisRoute(this.$route, ['SharedWithMe']) && !this.$route.params.id
},
canCreateFolder() {
return !this.$isThisRoute(this.$route, ['Files', 'Public', 'TeamFolders', 'SharedWithMe'])
},
canShowConvertToTeamFolder() {
return this.$isThisRoute(this.$route, ['Files', 'MySharedItems'])
return !this.$isThisRoute(this.$route, ['Files', 'Public'])
},
canUploadInView() {
return !this.$isThisRoute(this.$route, ['Files', 'RecentUploads', 'Public', 'TeamFolders', 'SharedWithMe'])
return !this.$isThisRoute(this.$route, ['Files', 'RecentUploads', 'Public'])
},
canUploadFolderInView() {
return !this.$isThisRoute(this.$route, ['Files', 'Public', 'TeamFolders', 'SharedWithMe'])
return !this.$isThisRoute(this.$route, ['Files', 'Public'])
},
canDeleteInView() {
let routes = ['TeamFolders', 'SharedWithMe', 'RecentUploads', 'MySharedItems', 'Trash', 'Public', 'Files']
let routes = ['RecentUploads', 'MySharedItems', 'Trash', 'Public', 'Files']
return !this.$isThisRoute(this.$route, routes) || this.clipboard.length === 0
},
canMoveInView() {
let routes = ['SharedWithMe', 'RecentUploads', 'MySharedItems', 'Public', 'Files', 'TeamFolders']
let routes = ['RecentUploads', 'MySharedItems', 'Public', 'Files']
return !this.$isThisRoute(this.$route, routes) || this.clipboard.length === 0
},
canShareInView() {
let routes = ['TeamFolders', 'RecentUploads', 'MySharedItems', 'Public', 'Files']
let routes = ['RecentUploads', 'MySharedItems', 'Public', 'Files']
return !this.$isThisRoute(this.$route, routes) || this.clipboard.length > 1 || this.clipboard.length === 0
},
canCreateTeamFolder() {
return (
this.$isThisRoute(this.$route, ['MySharedItems', 'Files']) &&
this.clipboard.length === 1 &&
this.clipboard[0].data.type === 'folder'
)
},
},
methods: {
showTeamFolderMenu() {
if (this.teamFolder) events.$emit('popover:open', 'team-folder')
},
showCreateMenu() {
events.$emit('popover:open', 'desktop-create')
},
@@ -1,144 +0,0 @@
<template>
<div class="hidden lg:block">
<div class="flex items-center justify-between py-3">
<NavigationBar />
<div class="flex items-center">
<!--I am Done-->
<div @click="uploadingDone" class="bg-theme-200 mr-6 flex cursor-pointer items-center rounded-lg py-1 pr-1 pl-4">
<b class="text-theme mr-3 text-sm leading-3">
{{ $t('tell_you_are_done', {name: uploadRequest.data.relationships.user.data.attributes.name}) }}
</b>
<MemberAvatar
:member="uploadRequest.data.relationships.user"
:size="34"
/>
</div>
<!--Create button-->
<PopoverWrapper>
<ToolbarButton
@click.stop.native="showCreateMenu"
source="cloud-plus"
:action="$t('create_something')"
/>
<PopoverItem name="desktop-create" side="left">
<OptionGroup :title="$t('frequently_used')">
<OptionUpload :title="$t('upload_files')" type="file" />
<Option
@click.native="$createFolder"
:title="$t('create_folder')"
icon="folder-plus"
/>
</OptionGroup>
<OptionGroup :title="$t('others')">
<OptionUpload :title="$t('upload_folder')" type="folder" />
<Option
@click.stop.native="$openRemoteUploadPopup"
:title="$t('remote_upload')"
icon="remote-upload"
/>
</OptionGroup>
</PopoverItem>
</PopoverWrapper>
<!--File Controls-->
<div v-if="!$isMobile()" class="ml-5 flex items-center xl:ml-8">
<ToolbarButton
@click.native="$moveFileOrFolder(clipboard[0])"
:class="{
'is-inactive': !canManipulate,
}"
source="move"
:action="$t('move')"
/>
<ToolbarButton
@click.native="$deleteFileOrFolder(clipboard[0])"
:class="{
'is-inactive': !canManipulate,
}"
source="trash"
:action="$t('delete')"
/>
</div>
<!--View Controls-->
<div class="ml-5 flex items-center xl:ml-8">
<PopoverWrapper>
<ToolbarButton
@click.stop.native="showSortingMenu"
source="preview-sorting"
:action="$t('sorting_view')"
/>
<PopoverItem name="desktop-sorting" side="left">
<FileSortingOptions />
</PopoverItem>
</PopoverWrapper>
<ToolbarButton
@click.native="$store.dispatch('fileInfoToggle')"
:action="$t('info_panel')"
source="info"
/>
</div>
</div>
</div>
<UploadProgress />
</div>
</template>
<script>
import PopoverWrapper from '../../UI/Popover/PopoverWrapper'
import FileSortingOptions from '../../Menus/FileSortingOptions'
import PopoverItem from '../../UI/Popover/PopoverItem'
import UploadProgress from '../../UI/Others/UploadProgress'
import NavigationBar from './NavigationBar'
import ToolbarButton from '../../UI/Buttons/ToolbarButton'
import MemberAvatar from "../../UI/Others/MemberAvatar"
import OptionUpload from '../../Menus/Components/OptionUpload'
import OptionGroup from '../../Menus/Components/OptionGroup'
import { events } from '../../../bus'
import { mapGetters } from 'vuex'
import Option from '../../Menus/Components/Option'
export default {
name: 'DesktopUploadRequestToolbar',
components: {
FileSortingOptions,
UploadProgress,
PopoverWrapper,
NavigationBar,
ToolbarButton,
MemberAvatar,
OptionUpload,
OptionGroup,
PopoverItem,
Option,
},
computed: {
...mapGetters(['isVisibleNavigationBars', 'currentTeamFolder', 'currentFolder', 'sharedDetail', 'clipboard', 'uploadRequest']),
canManipulate() {
return this.clipboard[0]
},
},
methods: {
uploadingDone() {
events.$emit('confirm:open', {
title: this.$t('closing_request_for_upload', {name: this.uploadRequest.data.relationships.user.data.attributes.name}),
message: this.$t('closing_request_for_upload_warn'),
action: {
id: this.$router.currentRoute.params.token,
operation: 'close-upload-request',
},
})
},
showCreateMenu() {
events.$emit('popover:open', 'desktop-create')
},
showSortingMenu() {
events.$emit('popover:open', 'desktop-sorting')
},
},
}
</script>
@@ -5,12 +5,6 @@
<NavigationBar />
<div class="relative flex items-center">
<TeamMembersButton
v-if="$isThisRoute($route, ['TeamFolders', 'SharedWithMe'])"
size="28"
@click.stop.native="$showMobileMenu('team-menu')"
class="absolute right-10"
/>
<!--More Actions-->
<div class="flex items-center relative mr-[4px]">
@@ -24,8 +18,6 @@
</template>
<script>
import TeamMembersPreview from '../../Teams/Components/TeamMembersPreview'
import TeamMembersButton from '../../Teams/Components/TeamMembersButton'
import { MenuIcon } from 'vue-feather-icons'
import NavigationBar from './NavigationBar'
@@ -33,8 +25,6 @@ export default {
name: 'MobileToolBar',
components: {
NavigationBar,
TeamMembersPreview,
TeamMembersButton,
MenuIcon,
},
methods: {
@@ -1,30 +0,0 @@
<template>
<div
class="sticky top-0 z-[19] block flex w-full items-center justify-between bg-white py-5 px-4 text-center dark:bg-dark-background lg:hidden"
>
<NavigationBar />
</div>
</template>
<script>
import TeamMembersPreview from '../../Teams/Components/TeamMembersPreview'
import TeamMembersButton from '../../Teams/Components/TeamMembersButton'
import { MenuIcon } from 'vue-feather-icons'
import NavigationBar from './NavigationBar'
export default {
name: 'MobileUploadRequestToolBar',
components: {
NavigationBar,
TeamMembersPreview,
TeamMembersButton,
MenuIcon,
},
methods: {
showMobileNavigation() {
this.$showMobileMenu('user-navigation')
this.$store.commit('DISABLE_MULTISELECT_MODE')
},
},
}
</script>
@@ -1,24 +0,0 @@
<template>
<MenuMobile name="team-menu">
<TeamFolderPreview />
<MenuMobileGroup v-if="$slots.default">
<slot />
</MenuMobileGroup>
</MenuMobile>
</template>
<script>
import MenuMobileGroup from '../Mobile/MenuMobileGroup'
import TeamFolderPreview from '../Teams/Components/TeamFolderPreview'
import MenuMobile from '../Mobile/MenuMobile'
export default {
name: 'MobileTeamContextMenu',
components: {
TeamFolderPreview,
MenuMobileGroup,
MenuMobile,
},
}
</script>
@@ -3,21 +3,6 @@
<!--User avatar-->
<UserHeadline v-if="!clickedSubmenu" class="p-5 pb-3" />
<!--User estimate-->
<div
v-if="config.subscriptionType === 'metered' && user && user.data.meta.usages && !clickedSubmenu"
class="block px-5 pt-2"
>
<div class="rounded-lg bg-light-background px-3 py-1.5 dark:bg-4x-dark-foreground">
<span class="text-sm font-semibold">
{{ $t('current_estimated_usage') }}
</span>
<span class="text-theme text-sm font-bold">
{{ user.data.meta.usages.costEstimate }}
</span>
</div>
</div>
<!--Go back button-->
<div v-if="clickedSubmenu" @click.stop="showSubmenu(undefined)" class="flex items-center p-5 pb-4">
<chevron-left-icon size="19" class="vue-feather text-theme mr-2 -ml-1" />
@@ -76,13 +61,6 @@
icon="hard-drive"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Billing')"
v-if="config.subscriptionType !== 'none'"
:title="$t('billing')"
icon="cloud"
:is-hover-disabled="true"
/>
</OptionGroup>
<!--Submenu: Admin settings-->
@@ -105,16 +83,6 @@
icon="settings"
:is-hover-disabled="true"
/>
</OptionGroup>
<!--Submenu: Content settings-->
<OptionGroup v-if="clickedSubmenu === 'admin'">
<Option
@click.native="goToRoute('Pages')"
:title="$t('pages')"
icon="monitor"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Language')"
:title="$t('languages')"
@@ -122,35 +90,6 @@
:is-hover-disabled="true"
/>
</OptionGroup>
<!--Submenu: Billing settings-->
<OptionGroup v-if="clickedSubmenu === 'admin' && config.subscriptionType !== 'none'">
<Option
@click.native="goToRoute('AppPayments')"
:title="$t('payments')"
icon="credit-card"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Subscriptions')"
v-if="config.subscriptionType === 'fixed'"
:title="$t('subscriptions')"
icon="credit-card"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Plans')"
:title="$t('plans')"
icon="database"
:is-hover-disabled="true"
/>
<Option
@click.native="goToRoute('Invoices')"
:title="$t('transactions')"
icon="file-text"
:is-hover-disabled="true"
/>
</OptionGroup>
</MenuMobileGroup>
</MenuMobile>
</template>
@@ -1,161 +0,0 @@
<template>
<article
class="delay-[3000ms] duration-700 transition-all relative z-[11] mb-1.5 flex items-start space-x-4 rounded-xl p-2.5"
:class="{'dark:bg-4x-dark-foreground bg-light-background/80': isUnread}"
>
<gift-icon
v-if="notification.data.attributes.category === 'gift'"
size="22"
class="vue-feather text-theme shrink-0"
/>
<user-plus-icon
v-if="notification.data.attributes.category === 'team-invitation'"
size="22"
class="vue-feather text-theme shrink-0"
/>
<trending-up-icon
v-if="notification.data.attributes.category === 'subscription-created'"
size="22"
class="vue-feather text-theme shrink-0"
/>
<alert-triangle-icon
v-if="['billing-alert', 'insufficient-balance'].includes(notification.data.attributes.category)"
size="22"
class="vue-feather text-theme shrink-0"
/>
<upload-cloud-icon
v-if="['file-request', 'remote-upload-done'].includes(notification.data.attributes.category)"
size="22"
class="vue-feather text-theme shrink-0"
/>
<div>
<b class="mb-1.5 block font-extrabold">
{{ notification.data.attributes.title }}
</b>
<p class="mb-1.5 text-sm dark:text-gray-500">
{{ notification.data.attributes.description }}
</p>
<div class="flex items-center">
<!--<MemberAvatar class="mr-2" :size="22" :is-border="false" :member="user" />-->
<time class="block text-xs text-gray-400 dark:text-gray-400">
{{ notification.data.attributes.created_at }}
</time>
</div>
<!--Accept or decline team invitation-->
<div v-if="action && action.type === 'invitation'" class="flex items-center space-x-3 mt-4">
<div
@click="acceptAction"
class="relative flex cursor-pointer items-center rounded-xl py-1.5 px-2 transition-colors bg-green-100 dark:bg-green-900"
>
<refresh-cw-icon v-if="isAccepting" size="16" class="animate-spin left-0 right-0 mx-auto vue-feather text-green-600 dark:text-green-600 absolute justify-center" />
<check-icon size="16" class="vue-feather mr-1 text-green-600 dark:text-green-600" :class="{'opacity-0': isAccepting}" />
<span class="text-sm font-bold text-green-600 dark:text-green-600" :class="{'opacity-0': isAccepting}">
{{ $t('accept') }}
</span>
</div>
<div
@click="declineAction"
class="relative flex cursor-pointer items-center rounded-xl py-1.5 px-2 transition-colors bg-rose-100 dark:bg-rose-900"
>
<refresh-cw-icon v-if="isDeclining" size="16" class="animate-spin left-0 right-0 mx-auto vue-feather text-rose-600 dark:text-rose-600 absolute justify-center" />
<x-icon size="16" class="vue-feather mr-1 text-rose-600 dark:text-rose-600" :class="{'opacity-0': isDeclining}" />
<span class="text-sm font-bold text-rose-600 dark:text-rose-600 capitalize" :class="{'opacity-0': isDeclining}">
{{ $t('decline') }}
</span>
</div>
</div>
<!--Go to route-->
<router-link
@click.native="closeCenter"
v-if="action && action.type === 'route'"
:to="{ name: action.params.route, params: { id: action.params.id } }"
class="mt-4 flex items-center"
>
<span class="mr-2 whitespace-nowrap text-xs font-bold">
{{ action.params.button }}
</span>
<chevron-right-icon size="16" class="text-theme vue-feather" />
</router-link>
</div>
</article>
</template>
<script>
import { RefreshCwIcon, TrendingUpIcon, GiftIcon, CheckIcon, XIcon, MailIcon, UserPlusIcon, UploadCloudIcon, ChevronRightIcon, AlertTriangleIcon } from 'vue-feather-icons'
import MemberAvatar from '../../UI/Others/MemberAvatar'
import {events} from "../../../bus";
export default {
name: 'Notification',
props: ['notification'],
components: {
MemberAvatar,
AlertTriangleIcon,
ChevronRightIcon,
UploadCloudIcon,
TrendingUpIcon,
RefreshCwIcon,
UserPlusIcon,
CheckIcon,
GiftIcon,
MailIcon,
XIcon,
},
computed: {
action() {
return this.notification.data.attributes.action
},
},
data() {
return {
isUnread: false,
isAccepting: false,
isDeclining: false,
}
},
methods: {
acceptAction() {
this.isAccepting = true
axios.put(`/api/teams/invitations/${this.notification.data.attributes.action.params.id}`)
.then(() => {
this.$store.commit('CLEAR_NOTIFICATION_ACTION_DATA', this.notification.data.id)
events.$emit('toaster', {
type: 'success',
message: this.$t('you_accepted_invitation'),
})
})
.finally(() => this.isAccepting = false)
},
declineAction() {
this.isDeclining = true
axios.delete(`/api/teams/invitations/${this.notification.data.attributes.action.params.id}`)
.then(() => {
this.$store.commit('CLEAR_NOTIFICATION_ACTION_DATA', this.notification.data.id)
events.$emit('toaster', {
type: 'success',
message: this.$t('you_decline_invitation'),
})
})
.finally(() => this.isDeclining = false)
},
closeCenter() {
this.$store.commit('CLOSE_NOTIFICATION_CENTER')
this.$closePopup()
},
},
created() {
this.isUnread = this.notification.data.attributes.read_at === null
setTimeout(() => this.isUnread = false, 1000)
}
}
</script>
@@ -1,25 +0,0 @@
<template>
<div class="relative button-icon inline-block cursor-pointer rounded-xl p-3">
<bell-icon size="18" class="vue-feather dark:text-gray-100 transition" :class="{'rotate-[30deg]': notificationCount}" />
<span v-if="notificationCount" class="absolute z-[9] right-1.5 bottom-1.5 flex items-center justify-center w-4 h-4 bg-theme text-white rounded-full text-xs font-bold">
{{ notificationCount }}
</span>
<span v-if="notificationCount" class="animate-ping absolute z-[8] right-1.5 bottom-1.5 flex items-center justify-center w-4 h-4 bg-theme text-white rounded-full text-xs font-bold"></span>
</div>
</template>
<script>
import {mapGetters} from "vuex";
import {BellIcon} from "vue-feather-icons"
export default {
name: 'NotificationBell',
components: {
BellIcon,
},
computed: {
...mapGetters([
'notificationCount'
]),
}
}
</script>
@@ -1,101 +0,0 @@
<template>
<transition name="popup">
<div class="fixed popup z-20 top-[27px] bottom-[27px] left-20 w-[360px]">
<!--Triangle-->
<div class="z-20 absolute left-0 top-[64px] w-4 translate-x-[-15px] overflow-hidden inline-block" :class="{'!top-[102px]': config.subscriptionType === 'metered'}">
<div class="h-12 -rotate-45 transform origin-top-right dark:bg-2x-dark-foreground bg-white bg-opacity-80 backdrop-blur-2xl"></div>
</div>
<div v-click-outside="clickOutside" class="dark:bg-2x-dark-foreground bg-white dark:bg-opacity-80 dark:backdrop-blur-2xl bg-opacity-80 backdrop-blur-2xl shadow-xl rounded-xl text-left p-3 overflow-y-auto max-h-full">
<!--Title-->
<b class="dark:text-gray-200 text-xl font-extrabold px-2.5 mb-2.5 block">
{{ $t('notification_center') }}
</b>
<div class="px-2.5">
<MobileActionButton v-if="readNotifications.length || unreadNotifications.length" @click.native="$store.dispatch('deleteAllNotifications')" icon="check-square" class="mb-2 dark:!bg-4x-dark-foreground">
{{ $t('clear_all') }}
</MobileActionButton>
<p v-if="!readNotifications.length && !unreadNotifications.length" class="text-sm mt-8">
{{ $t("not_any_notifications") }}
</p>
</div>
<b v-if="unreadNotifications.length" class="dark-text-theme mt-1.5 block px-2.5 mb-2.5 text-xs text-gray-400">
{{ $t('unread') }}
</b>
<Notification :notification="notification" v-for="notification in unreadNotifications" :key="notification.id" />
<b v-if="readNotifications.length" class="dark-text-theme mt-2.5 block px-2.5 mb-2.5 text-xs text-gray-400">
{{ $t('read') }}
</b>
<Notification :notification="notification" v-for="notification in readNotifications" :key="notification.id" />
</div>
</div>
</transition>
</template>
<script>
import MobileActionButton from "../UI/Buttons/MobileActionButton"
import Notification from "./Components/Notification"
import vClickOutside from 'v-click-outside'
import {mapGetters} from "vuex";
export default {
name: 'NotificationCenter',
components: {
MobileActionButton,
Notification
},
directives: {
clickOutside: vClickOutside.directive
},
computed: {
...mapGetters([
'user', 'config', 'isVisibleNotificationCenter',
]),
readNotifications() {
return this.user.data.relationships.readNotifications.data
},
unreadNotifications() {
return this.user.data.relationships.unreadNotifications.data
}
},
methods: {
clickOutside() {
if (this.isVisibleNotificationCenter)
this.$store.commit('CLOSE_NOTIFICATION_CENTER')
},
},
created() {
this.$store.dispatch('readAllNotifications')
}
}
</script>
<style lang="scss" scoped>
.popup-leave-active {
animation: popup-slide-in 0.15s ease reverse;
}
.popup-enter-active {
animation: popup-slide-in 0.25s 0.1s ease both;
}
@keyframes popup-slide-in {
0% {
opacity: 0;
transform: translateY(50px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
</style>
@@ -1,94 +0,0 @@
<template>
<PopupWrapper name="notifications-mobile">
<!--Title-->
<PopupHeader :title="$t('notifications')" icon="bell" />
<!--Content-->
<PopupContent v-if="readNotifications && unreadNotifications">
<MobileActionButton
v-if="readNotifications.length || unreadNotifications.length"
@click.native="$store.dispatch('deleteAllNotifications')"
icon="check-square"
class="mb-2 dark:!bg-4x-dark-foreground"
>
{{ $t('clear_all') }}
</MobileActionButton>
<p v-if="!readNotifications.length && !unreadNotifications.length" class="text-sm text-gray-500">
{{ $t("not_any_notifications") }}
</p>
<b
v-if="unreadNotifications.length"
class="dark-text-theme mt-1.5 mb-2.5 block px-2.5 text-xs text-gray-400"
>
{{ $t('unread') }}
</b>
<Notification
:notification="notification"
v-for="notification in unreadNotifications"
:key="notification.id"
/>
<b v-if="readNotifications.length" class="dark-text-theme mt-2.5 mb-2.5 block px-2.5 text-xs text-gray-400">
{{ $t('read') }}
</b>
<Notification
:notification="notification"
v-for="notification in readNotifications"
:key="notification.id"
/>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary">
{{ $t('close') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import MobileActionButton from '../UI/Buttons/MobileActionButton'
import Notification from './Components/Notification'
import ButtonBase from '../UI/Buttons/ButtonBase'
import PopupWrapper from '../Popups/Components/PopupWrapper'
import PopupActions from '../Popups/Components/PopupActions'
import PopupContent from '../Popups/Components/PopupContent'
import PopupHeader from '../Popups/Components/PopupHeader'
import vClickOutside from 'v-click-outside'
import { mapGetters } from 'vuex'
export default {
name: 'NotificationsPopup',
components: {
MobileActionButton,
Notification,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
},
directives: {
clickOutside: vClickOutside.directive,
},
computed: {
...mapGetters(['user', 'config']),
readNotifications() {
return this.user?.data.relationships.readNotifications.data
},
unreadNotifications() {
return this.user?.data.relationships.unreadNotifications.data
},
},
methods: {
clickOutside() {
if (this.isVisibleNotificationCenter) this.$store.commit('CLOSE_NOTIFICATION_CENTER')
},
},
}
</script>
@@ -1,164 +0,0 @@
<template>
<PopupWrapper name="remote-upload">
<PopupHeader :title="$t('upload_files_remotely')" icon="remote-upload" />
<PopupContent>
<ValidationObserver @submit.prevent ref="createForm" v-slot="{ invalid }" tag="form">
<ValidationProvider tag="div" mode="passive" name="Remote Links" rules="required" v-slot="{ errors }">
<AppInputText
:title="$t('remote_links')"
:description="$t('remote_links_help')"
:error="errors[0]"
:is-last="true"
>
<textarea
v-model="links"
class="focus-border-theme input-dark whitespace-nowrap"
rows="6"
:placeholder="$t('paste_remote_links_here')"
:class="{ '!border-rose-600': errors[0] }"
ref="textarea"
>
</textarea>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</PopupContent>
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary">
{{ $t('cancel') }}
</ButtonBase>
<ButtonBase class="w-full" @click.native="upload" button-style="theme" :loading="loading">
{{ $t('upload') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import PopupWrapper from '../Popups/Components/PopupWrapper'
import PopupContent from '../Popups/Components/PopupContent'
import PopupActions from '../Popups/Components/PopupActions'
import PopupHeader from '../Popups/Components/PopupHeader'
import AppInputText from '../Forms/Layouts/AppInputText'
import { required } from 'vee-validate/dist/rules'
import ButtonBase from '../UI/Buttons/ButtonBase'
import { events } from '../../bus'
import i18n from "../../i18n";
export default {
name: 'RemoteUploadPopup',
components: {
ValidationProvider,
ValidationObserver,
required,
PopupWrapper,
PopupContent,
PopupHeader,
PopupActions,
AppInputText,
ButtonBase,
},
data() {
return {
links: undefined,
loading: false,
}
},
methods: {
async upload() {
// Validate fields
const isValid = await this.$refs.createForm.validate()
if (!isValid) return
this.loading = true
this.urls = this.links
.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, "")
.split(/\r?\n/)
// If demo, return success message
if (this.$store.getters.config.isDemo && this.$store.getters.user.data.attributes.email === 'ho**@hi5ve.digital') {
events.$emit('toaster', {
type: 'success',
message: i18n.t('remote_download_finished'),
})
events.$emit('popup:close')
return
}
// If broadcasting
if (this.$store.getters.isBroadcasting) {
this.$store.commit('UPDATE_REMOTE_UPLOAD_QUEUE', {
progress: {
total: this.urls.length,
processed: 0,
failed: 0,
}
})
}
// Get route
let route = {
RequestUpload: `/api/upload-request/${this.$router.currentRoute.params.token}/upload/remote`,
Public: `/api/editor/upload/remote/${this.$router.currentRoute.params.token}`,
}[this.$router.currentRoute.name] || '/api/upload/remote'
let parentId = this.$store.getters.currentFolder
? this.$store.getters.currentFolder.data.id
: undefined
axios
.post(route, {
urls: this.urls,
parent_id: parentId,
})
.then(() => {
// If broadcasting is not set
if (!this.$store.getters.isBroadcasting) {
// Reload data
this.$getDataByLocation()
events.$emit('toaster', {
type: 'success',
message: i18n.t('remote_download_finished'),
})
}
events.$emit('popup:close')
})
.catch((error) => {
if (error.response.status === 422) {
this.$refs.createForm.setErrors({
'Remote Links': error.response.data.message,
})
}
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.loading = false
})
},
},
mounted() {
events.$on('popup:open', (args) => {
if (args.name !== 'remote-upload') return
this.links = undefined
this.$nextTick(() => {
setTimeout(() => this.$refs.textarea.focus(), 100)
})
})
},
}
</script>
@@ -1,54 +0,0 @@
<template>
<transition name="popup">
<div v-if="remoteUploadQueue" class="fixed left-0 right-0 bottom-8 text-center select-none pointer-events-none z-10">
<div class="relative inline-block rounded-lg overflow-hidden bg-theme shadow-lg px-3 py-2">
<div class="flex items-center">
<RefreshCwIcon size="14" class="vue-feather text-white animate-[spin_2s_linear_infinite] z-10 relative" />
<span class="text-xs font-bold text-white z-10 relative ml-2 dark:text-black">
{{ this.$t('remote_upload_progress', {processed: remoteUploadQueue.processed, total: remoteUploadQueue.total}) }}{{ remoteUploadQueue.failed > 0 ? ", " + this.$t('remote_upload_failed_count', {count: remoteUploadQueue.failed}) : '' }}
</span>
</div>
<span class="absolute w-full h-full top-0 bottom-0 left-0 right-0 block bg-theme brightness-125 animate-[pulse_3s_ease-in-out_infinite] z-[5]"></span>
<span class="absolute w-full h-full top-0 bottom-0 left-0 right-0 block bg-theme z-0"></span>
</div>
</div>
</transition>
</template>
<script>
import {RefreshCwIcon} from "vue-feather-icons";
import {mapGetters} from "vuex";
export default {
name: 'RemoteUploadProgress',
components: {
RefreshCwIcon,
},
computed: {
...mapGetters([
'remoteUploadQueue'
]),
},
}
</script>
<style lang="scss">
.popup-leave-active {
animation: popup-slide-in 0.15s ease reverse;
}
.popup-enter-active {
animation: popup-slide-in 0.25s 0.1s ease both;
}
@keyframes popup-slide-in {
0% {
opacity: 0;
transform: translateY(100px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
</style>
@@ -7,26 +7,6 @@
<div v-if="user" class="mb-auto text-center">
<MemberAvatar class="mx-auto" :size="44" :is-border="false" :member="user" />
<!--Usage-->
<div
v-if="config.subscriptionType === 'metered' && user.data.meta.usages"
class="mt-2.5 text-center leading-3"
>
<b class="text-theme block text-xs font-bold leading-3">
{{ user.data.meta.usages.costEstimate }}
</b>
<span class="text-xs text-gray-500">
{{ $t('usage') }}
</span>
</div>
<!--Navigation-->
<div class="mt-2 relative">
<NotificationBell @click.native="$store.commit('TOGGLE_NOTIFICATION_CENTER')" class="hover:bg-light-300 dark:hover:bg-4x-dark-foreground" />
</div>
<NotificationCenter v-if="isVisibleNotificationCenter" />
<!--Navigation-->
<div class="mt-6">
<router-link
@@ -75,14 +55,10 @@
import MemberAvatar from '../UI/Others/MemberAvatar'
import {mapGetters} from 'vuex'
import {HardDriveIcon, MoonIcon, PowerIcon, SettingsIcon, SunIcon, UserIcon,} from 'vue-feather-icons'
import NotificationCenter from "../Notifications/NotificationCenter"
import NotificationBell from "../Notifications/Components/NotificationBell";
export default {
name: 'SidebarNavigation',
components: {
NotificationBell,
NotificationCenter,
HardDriveIcon,
SettingsIcon,
MemberAvatar,
@@ -92,7 +68,7 @@ export default {
SunIcon,
},
computed: {
...mapGetters(['isVisibleNavigationBars', 'isDarkMode', 'config', 'user', 'isVisibleNotificationCenter']),
...mapGetters(['isVisibleNavigationBars', 'isDarkMode', 'config', 'user']),
navigation() {
if (this.user.data.attributes.role === 'admin') {
return [
@@ -133,11 +109,6 @@ export default {
]
},
},
data() {
return {
isNotificationCenter: false,
}
},
methods: {
isSection(section) {
return this.$route.matched[0].name === section
+2 -190
View File
@@ -99,11 +99,6 @@
size="18"
class="vue-feather text-theme"
/>
<credit-card-icon
v-if="result.action.value === 'AppPayments'"
size="18"
class="vue-feather text-theme"
/>
<home-icon
v-if="result.action.value === 'Files'"
size="18"
@@ -114,23 +109,13 @@
size="18"
class="vue-feather text-theme"
/>
<database-icon
v-if="['CreateFixedPlan', 'CreateMeteredPlan'].includes(result.action.value)"
size="18"
class="vue-feather text-theme"
/>
<user-plus-icon
v-if="result.action.value === 'UserCreate'"
size="18"
class="vue-feather text-theme"
/>
<users-icon
v-if="['TeamFolders', 'Users'].includes(result.action.value)"
size="18"
class="vue-feather text-theme"
/>
<user-check-icon
v-if="result.action.value === 'SharedWithMe'"
v-if="['Users'].includes(result.action.value)"
size="18"
class="vue-feather text-theme"
/>
@@ -139,41 +124,16 @@
size="18"
class="vue-feather text-theme"
/>
<link2-icon
v-if="result.action.value === 'remote-upload'"
size="18"
class="vue-feather text-theme"
/>
<upload-cloud-icon
v-if="result.action.value === 'RecentUploads'"
size="18"
class="vue-feather text-theme"
/>
<file-text-icon
v-if="['Invoices', 'Invoice'].includes(result.action.value)"
size="18"
class="vue-feather text-theme"
/>
<database-icon
v-if="result.action.value === 'Plans'"
size="18"
class="vue-feather text-theme"
/>
<dollar-sign-icon
v-if="['Subscriptions', 'Billing'].includes(result.action.value)"
size="18"
class="vue-feather text-theme"
/>
<globe-icon
v-if="result.action.value === 'Language'"
size="18"
class="vue-feather text-theme"
/>
<monitor-icon
v-if="result.action.value === 'Pages'"
size="18"
class="vue-feather text-theme"
/>
<box-icon
v-if="result.action.value === 'Dashboard'"
size="18"
@@ -214,16 +174,6 @@
size="18"
class="vue-feather text-theme"
/>
<folder-plus-icon
v-if="result.action.value === 'create-team-folder'"
size="18"
class="vue-feather text-theme"
/>
<upload-cloud-icon
v-if="result.action.value === 'create-file-request'"
size="18"
class="vue-feather text-theme"
/>
<b class="ml-3.5 text-sm font-bold">
{{ result.title }}
@@ -397,20 +347,6 @@ export default {
value: 'AppOthers',
},
},
{
title: this.$t('go_to_payments'),
action: {
type: 'route',
value: 'AppPayments',
},
},
{
title: this.$t('go_to_pages'),
action: {
type: 'route',
value: 'Pages',
},
},
{
title: this.$t('go_to_languages'),
action: {
@@ -425,20 +361,6 @@ export default {
value: 'Users',
},
},
{
title: this.$t('show_all_plans'),
action: {
type: 'route',
value: 'Plans',
},
},
{
title: this.$t('show_transactions'),
action: {
type: 'route',
value: 'Invoices',
},
},
{
title: this.$t('application_settings'),
action: {
@@ -446,13 +368,6 @@ export default {
value: 'AppOthers',
},
},
{
title: this.$t('login_registration_settings'),
action: {
type: 'route',
value: 'AppSignInUp',
},
},
{
title: this.$t('appearance_settings'),
action: {
@@ -460,20 +375,6 @@ export default {
value: 'AppAppearance',
},
},
{
title: this.$t('adsense_settings'),
action: {
type: 'route',
value: 'AppAdsense',
},
},
{
title: this.$t('homepage_settings'),
action: {
type: 'route',
value: 'AppIndex',
},
},
{
title: this.$t('environment_settings'),
action: {
@@ -519,20 +420,6 @@ export default {
value: 'Trash',
},
},
{
title: this.$t('go_to_team_folders'),
action: {
type: 'route',
value: 'TeamFolders',
},
},
{
title: this.$t('go_to_shared_with_me'),
action: {
type: 'route',
value: 'SharedWithMe',
},
},
]
let adminActions = [
@@ -567,13 +454,6 @@ export default {
value: 'Storage',
},
},
{
title: this.$t('show_billing'),
action: {
type: 'route',
value: 'Billing',
},
},
{
title: this.$t('empty_your_trash'),
action: {
@@ -590,29 +470,7 @@ export default {
},
]
let createList = [
{
title: this.$t('create_team_folder'),
action: {
type: 'function',
value: 'create-team-folder',
},
},
{
title: this.$t('create_file_request'),
action: {
type: 'function',
value: 'create-file-request',
},
},
{
title: this.$t('remote_upload'),
action: {
type: 'function',
value: 'remote-upload',
},
},
]
let createList = []
let functionList = [
{
@@ -656,31 +514,6 @@ export default {
// Return commands for logged admin
if (this.isAdmin) {
// Available only for fixed subscription
if (this.config.subscriptionType === 'fixed') {
adminLocations.push({
title: this.$t('show_all_subscriptions'),
action: {
type: 'route',
value: 'Subscriptions',
},
})
}
// Available only when is metered billing and plan doesnt exist or when is fixed billing
if (
(this.config.subscriptionType === 'metered' && !this.config.isCreatedMeteredPlan) ||
this.config.subscriptionType === 'fixed'
) {
adminActions.push({
title: this.$t('create_plan'),
action: {
type: 'route',
value: this.config.subscriptionType === 'fixed' ? 'CreateFixedPlan' : 'CreateMeteredPlan',
},
})
}
return [].concat.apply(
[],
[functionList, createList, userSettings, fileLocations, adminLocations, adminActions]
@@ -819,18 +652,6 @@ export default {
if (arg.action.value === 'empty-trash') {
this.$emptyTrashQuietly()
}
if (arg.action.value === 'create-team-folder') {
this.$createTeamFolder()
}
if (arg.action.value === 'create-file-request') {
this.$createFileRequest()
}
if (arg.action.value === 'remote-upload') {
this.$openRemoteUploadPopup()
}
}
this.exitSpotlight()
@@ -854,15 +675,6 @@ export default {
id: file.data.id,
},
})
} else if (file.data.attributes.isTeamFolder) {
let route = file.data.relationships.user.data.id === this.user.data.id
? 'TeamFolders'
: 'SharedWithMe'
this.$router.push({
name: route,
params: { id: file.data.id },
})
} else {
this.$router.push({
name: 'Files',
@@ -1,98 +0,0 @@
<template>
<tr class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5">
<td class="py-5 pr-3 md:pr-1">
<span class="text-sm font-bold">
{{ row.data.attributes.note }}
</span>
</td>
<td v-if="user" class="whitespace-nowrap px-3 md:px-1">
<div v-if="row.data.relationships.user" class="flex items-center">
<MemberAvatar :is-border="false" :size="36" :member="row.data.relationships.user" />
<div class="ml-3 pr-10">
<b
class="max-w-1 block overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold"
style="max-width: 155px"
>
{{ row.data.relationships.user.data.attributes.name }}
</b>
<span class="block text-xs text-gray-600 dark:text-gray-500">
{{ row.data.relationships.user.data.attributes.email }}
</span>
</div>
</div>
<span v-if="!row.data.relationships.user" class="text-xs font-bold text-gray-500">
{{ $t('user_was_deleted') }}
</span>
</td>
<td class="px-3 md:px-1">
<ColorLabel class="capitalize" :color="$getTransactionStatusColor(row.data.attributes.status)">
{{ $t(row.data.attributes.status) }}
</ColorLabel>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold" :class="$getTransactionTypeTextColor(row.data.attributes.type)">
{{ $getTransactionMark(row.data.attributes.type) + row.data.attributes.price }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.created_at }}
</span>
</td>
<td class="px-3 md:px-1">
<div class="w-28">
<img
class="inline-block max-h-5"
:src="$getPaymentLogo(row.data.attributes.driver)"
:alt="row.data.attributes.driver"
/>
</div>
</td>
<td class="pl-3 text-right md:pl-1">
<div class="inline-block">
<a
:href="$getInvoiceLink(row.data.id)"
target="_blank"
class="inline-block flex h-8 w-8 cursor-pointer items-center justify-center rounded-md bg-light-background transition-colors hover:bg-purple-100 dark:bg-2x-dark-foreground"
>
<FileTextIcon size="15" class="opacity-75" />
</a>
</div>
</td>
</tr>
</template>
<script>
import MemberAvatar from '../UI/Others/MemberAvatar'
import MeteredTransactionDetailRow from './MeteredTransactionDetailRow'
import ColorLabel from '../UI/Labels/ColorLabel'
import { EyeIcon, FileTextIcon } from 'vue-feather-icons'
export default {
name: 'FixedTransactionRow',
components: {
MeteredTransactionDetailRow,
MemberAvatar,
FileTextIcon,
ColorLabel,
EyeIcon,
},
props: {
row: {},
user: {
type: Boolean,
default: false,
},
},
data() {
return {
showedTransactionDetailById: undefined,
}
},
methods: {
showTransactionDetail(id) {
if (this.showedTransactionDetailById === id) this.showedTransactionDetailById = undefined
else this.showedTransactionDetailById = id
},
},
}
</script>
@@ -1,36 +0,0 @@
<template>
<tr>
<td colspan="10" class="overflow-hidden rounded-lg py-2">
<div
class="flex items-center justify-between border-b border-dashed border-light py-2 dark:border-opacity-5"
v-for="(usage, i) in row.data.attributes.metadata"
:key="i"
>
<div class="w-2/4 leading-none">
<b class="text-sm font-bold leading-none">
{{ $t(usage.feature) }}
</b>
<small class="hidden pt-2 text-xs leading-none text-gray-500 sm:block">
{{ $t(`feature_usage_desc_${usage.feature}`) }}
</small>
</div>
<div class="w-1/4 text-left">
<span class="text-gray-560 text-sm font-bold">
{{ usage.usage }}
</span>
</div>
<div class="w-1/4 text-right">
<span class="text-theme text-sm font-bold">
{{ usage.cost }}
</span>
</div>
</div>
</td>
</tr>
</template>
<script>
export default {
name: 'MeteredTransactionDetailRow',
props: ['row'],
}
</script>
@@ -1,110 +0,0 @@
<template>
<tr class="whitespace-nowrap border-b border-dashed border-light dark:border-opacity-5">
<td class="py-5 pr-3 md:pr-1">
<span class="text-sm font-bold">
{{ row.data.attributes.note }}
</span>
</td>
<td v-if="user" class="whitespace-nowrap px-3 md:px-1">
<div v-if="row.data.relationships.user" class="flex items-center">
<MemberAvatar :is-border="false" :size="36" :member="row.data.relationships.user" />
<div class="ml-3 pr-10">
<b
class="max-w-1 block overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold"
style="max-width: 155px"
>
{{ row.data.relationships.user.data.attributes.name }}
</b>
<span class="block text-xs text-gray-600 dark:text-gray-500">
{{ row.data.relationships.user.data.attributes.email }}
</span>
</div>
</div>
<span v-if="!row.data.relationships.user" class="text-xs font-bold text-gray-500">
{{ $t('user_was_deleted') }}
</span>
</td>
<td class="px-3 md:px-1">
<ColorLabel class="capitalize" :color="$getTransactionStatusColor(row.data.attributes.status)">
{{ $t(row.data.attributes.status) }}
</ColorLabel>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold capitalize">
{{ $t(row.data.attributes.type) }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold" :class="$getTransactionTypeTextColor(row.data.attributes.type)">
{{ $getTransactionMark(row.data.attributes.type) + row.data.attributes.price }}
</span>
</td>
<td class="px-3 md:px-1">
<span class="text-sm font-bold">
{{ row.data.attributes.created_at }}
</span>
</td>
<td class="px-3 md:px-1">
<div class="w-28">
<img
class="inline-block max-h-5"
:src="$getPaymentLogo(row.data.attributes.driver)"
:alt="row.data.attributes.driver"
/>
</div>
</td>
<td class="pl-3 text-right md:pl-1">
<div v-if="row.data.attributes.metadata" class="flex w-full justify-end space-x-2">
<div
@click="$emit('showDetail', row.data.id)"
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md bg-light-background transition-colors hover:bg-green-100 dark:bg-2x-dark-foreground"
>
<EyeIcon size="15" class="opacity-75" />
</div>
<a
:href="$getInvoiceLink(row.data.id)"
target="_blank"
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md bg-light-background transition-colors hover:bg-purple-100 dark:bg-2x-dark-foreground"
>
<FileTextIcon size="15" class="opacity-75" />
</a>
</div>
<div v-else>-</div>
</td>
</tr>
</template>
<script>
import MemberAvatar from '../UI/Others/MemberAvatar'
import MeteredTransactionDetailRow from './MeteredTransactionDetailRow'
import ColorLabel from '../UI/Labels/ColorLabel'
import { EyeIcon, FileTextIcon } from 'vue-feather-icons'
export default {
name: 'MeteredTransactionRow',
components: {
MeteredTransactionDetailRow,
MemberAvatar,
FileTextIcon,
ColorLabel,
EyeIcon,
},
props: {
row: {},
user: {
type: Boolean,
default: false,
},
},
data() {
return {
showedTransactionDetailById: undefined,
}
},
methods: {
showTransactionDetail(id) {
if (this.showedTransactionDetailById === id) this.showedTransactionDetailById = undefined
else this.showedTransactionDetailById = id
},
},
}
</script>
@@ -1,59 +0,0 @@
<template>
<div
class="flex items-center justify-between rounded-lg bg-light-background py-3 px-2 dark:bg-2x-dark-foreground md:px-4"
>
<div class="flex items-center">
<img :src="`/assets/gateways/${card.data.attributes.brand}.svg`" alt="" class="mr-3 h-5 rounded" />
<b class="whitespace-nowrap text-sm font-bold capitalize leading-none">
{{ card.data.attributes.brand }}
{{ card.data.attributes.last4 }}
</b>
</div>
<b class="text-sm font-bold leading-none"> {{ $t('expires') }} {{ card.data.attributes.expiration }} </b>
<Trash2Icon @click="deleteCreditCard(card.data.id)" size="15" class="cursor-pointer" />
</div>
</template>
<script>
import { Trash2Icon } from 'vue-feather-icons'
import { events } from '../../bus'
import axios from 'axios'
export default {
name: 'PaymentCard',
components: {
Trash2Icon,
},
props: ['card'],
methods: {
deleteCreditCard(id) {
events.$emit('confirm:open', {
title: this.$t('want_to_delete_card_title'),
message: this.$t('want_to_delete_card_description'),
action: {
id: id,
operation: 'delete-credit-card',
},
})
},
},
created() {
events.$on('action:confirmed', (data) => {
if (data.operation === 'delete-credit-card')
axios
.delete(`/api/stripe/credit-cards/${data.id}`)
.then(() => {
this.$store.dispatch('getAppData')
events.$emit('toaster', {
type: 'success',
message: this.$t('credit_card_deleted'),
})
})
.catch(() => this.$isSomethingWrong())
})
},
destroyed() {
events.$off('action:confirmed')
},
}
</script>
@@ -1,19 +0,0 @@
<template>
<div class="flex items-center justify-between border-b border-dashed border-light py-4 dark:border-opacity-5">
<div>
<img :src="$getPaymentLogo(driver)" :alt="driver" class="h-6" />
<small class="block pt-2 text-xs leading-4 dark:text-gray-500 text-gray-500">
{{ description }}
</small>
</div>
<div v-if="$slots.default" class="bg-theme-200 relative inline-block rounded-lg px-3 py-1">
<slot />
</div>
</div>
</template>
<script>
export default {
name: 'PaymentMethod',
props: ['description', 'driver'],
}
</script>
@@ -1,45 +0,0 @@
<template>
<div
class="block cursor-pointer select-none rounded-lg py-3 px-4"
:class="{
'bg-light-background dark:bg-2x-dark-foreground': isSelected,
}"
>
<div class="mb-1.5 flex items-center justify-between">
<CheckBox :is-clicked="isSelected" />
<b class="flex-1 pl-4 text-left text-lg">
{{ plan.data.attributes.name }}
</b>
<span
class="text-theme bg-theme-100 ml-9 inline-block whitespace-nowrap rounded-xl py-1 px-2 text-sm font-extrabold"
>
{{ plan.data.attributes.price }} /
{{ $t(`interval.${plan.data.attributes.interval}`) }}
</span>
</div>
<ul class="ml-9 mb-3">
<li class="mb-1.5 flex items-center" v-for="(value, key, i) in plan.data.attributes.features" :key="i">
<CheckIcon size="12" class="svg-stroke-theme" />
<small class="pl-1.5 text-xs font-bold text-gray-600" v-if="value !== -1">
{{ $t(key === 'max_team_members' ? 'max_team_members_total' : key, { value: value }) }}
</small>
<small class="pl-1.5 text-xs font-bold text-gray-600" v-if="value === -1">
{{ $t(`${key}.unlimited`) }}
</small>
</li>
</ul>
</div>
</template>
<script>
import { CheckIcon } from 'vue-feather-icons'
import CheckBox from '../Inputs/CheckBox'
export default {
name: 'PlanDetail',
components: {
CheckIcon,
CheckBox,
},
props: ['isSelected', 'plan'],
}
</script>
@@ -1,37 +0,0 @@
<template>
<div class="mb-2 text-right">
<label
:class="{ 'text-gray-400': !isSelectedYearlyPlans }"
class="cursor-pointer text-xs font-bold"
>
{{ $t('billed_annually') }}
</label>
<div class="relative inline-block w-12 select-none align-middle">
<SwitchInput
class="scale-75 transform"
v-model="isSelectedYearlyPlans"
:state="isSelectedYearlyPlans"
/>
</div>
</div>
</template>
<script>
import SwitchInput from '../Inputs/SwitchInput'
export default {
name: 'PlanPeriodSwitcher',
components: {
SwitchInput
},
watch: {
'isSelectedYearlyPlans': function () {
this.$emit('input', this.isSelectedYearlyPlans)
}
},
data() {
return {
isSelectedYearlyPlans: false
}
}
}
</script>
@@ -1,196 +0,0 @@
<template>
<PopupWrapper name="change-plan-subscription">
<PopupHeader :title="$t('change_your_plan')" icon="credit-card" />
<!--Select Payment Plans-->
<PopupContent v-if="plans">
<InfoBox v-if="plans.data.length === 0" class="!mb-0">
<p>{{ $t("not_any_plan") }}</p>
</InfoBox>
<!--Toggle yearly billing-->
<PlanPeriodSwitcher v-if="yearlyPlans.length > 0" v-model="isSelectedYearlyPlans" />
<!--List available plans-->
<div>
<PlanDetail
v-for="(plan, i) in plans.data"
:plan="plan"
:key="plan.data.id"
v-if="plan.data.attributes.interval === intervalPlanType"
:class="{'opacity-50 pointer-events-none': userSubscribedPlanId === plan.data.id}"
:is-selected="selectedPlan && selectedPlan.data.id === plan.data.id"
@click.native="selectPlan(plan)"
/>
</div>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary"
>{{ $t('cancel') }}
</ButtonBase>
<ButtonBase
class="w-full"
v-if="plans && plans.data.length !== 0"
:button-style="buttonStyle"
:loading="isLoading"
@click.native="proceedToPayment"
>{{ $t('change_plan') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import PopupWrapper from '../../Popups/Components/PopupWrapper'
import PopupActions from '../../Popups/Components/PopupActions'
import PopupContent from '../../Popups/Components/PopupContent'
import PopupHeader from '../../Popups/Components/PopupHeader'
import ButtonBase from '../../UI/Buttons/ButtonBase'
import PlanDetail from '../PlanDetail'
import {mapGetters} from 'vuex'
import {events} from '../../../bus'
import axios from 'axios'
import Spinner from '../../UI/Others/Spinner'
import InfoBox from '../../UI/Others/InfoBox'
import PlanPeriodSwitcher from "../PlanPeriodSwitcher";
export default {
name: 'ChangeSubscriptionPopup',
components: {
PlanPeriodSwitcher,
InfoBox,
Spinner,
PlanDetail,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
},
watch: {
isSelectedYearlyPlans() {
this.selectedPlan = undefined
},
},
computed: {
...mapGetters(['config', 'user']),
intervalPlanType() {
return this.isSelectedYearlyPlans ? 'year' : 'month'
},
buttonStyle() {
return this.selectedPlan ? 'theme' : 'secondary'
},
userSubscribedPlanId() {
return (
this.user &&
this.user.data.relationships.subscription &&
this.user.data.relationships.subscription.data.relationships.plan.data.id
)
},
yearlyPlans() {
return this.plans.data.filter((plan) => plan.data.attributes.interval === 'year')
},
subscriptionDriver() {
return this.user.data.relationships.subscription.data.attributes.driver
},
subscription() {
return this.user.data.relationships.subscription
}
},
data() {
return {
isSelectedYearlyPlans: false,
isLoading: false,
selectedPlan: undefined,
plans: undefined,
}
},
methods: {
proceedToPayment() {
// Start button spinner
this.isLoading = true
if (this.subscriptionDriver === 'stripe') {
this.payByStripe()
}
if (this.subscriptionDriver === 'paypal') {
this.payByPayPal()
}
if (this.subscriptionDriver === 'paystack') {
this.payByPaystack()
}
},
payByPayPal() {
axios.post(`/api/subscriptions/swap/${this.selectedPlan.data.id}`)
.then((response) => {
window.location = response.data.links[0].href
})
},
payByStripe() {
// Subscribe to the new plan
if (['inactive', 'cancelled', 'completed'].includes(this.subscription.data.attributes.status)) {
axios
.post('/api/stripe/checkout', {
planCode: this.selectedPlan.data.meta.driver_plan_id.stripe,
})
.then((response) => {
window.location = response.data.url
})
.catch((error) => {
if (error.response.status === 500 && error.response.data.type) {
events.$emit('alert:open', {
title: error.response.data.title,
message: error.response.data.message,
})
} else {
this.$isSomethingWrong()
}
})
}
// Change active subscription
if (this.subscription.data.attributes.status === 'active') {
axios
.post(`/api/subscriptions/swap/${this.selectedPlan.data.id}`)
.then(() => {
this.$closePopup()
events.$emit('toaster', {
type: 'success',
message: this.$t('subscription_changed'),
})
})
}
},
payByPaystack() {
axios
.post('/api/paystack/checkout', {
planCode: this.selectedPlan.data.meta.driver_plan_id.paystack,
})
.then((response) => {
window.location = response.data.data.authorization_url
})
},
selectPlan(plan) {
this.selectedPlan = plan
},
},
created() {
// Load available plans
axios.get('/api/subscriptions/plans').then((response) => {
this.plans = response.data
})
// Reset states on popup close
events.$on('popup:close', () => {
this.isSelectedYearlyPlans = false
this.selectedPlan = undefined
})
},
}
</script>
@@ -1,182 +0,0 @@
<template>
<PopupWrapper name="select-payment-method">
<PopupHeader :title="$t('select_payment_method')" icon="credit-card" />
<PopupContent style="padding: 0 20px">
<InfoBox v-if="!config.isPayPal && !config.isPaystack" class="!mb-0">
<p>{{ $t("not_any_payment_method") }}</p>
</InfoBox>
<!--PayPal implementation-->
<div
v-if="config.isPayPal"
:class="{
'mb-2 rounded-xl bg-light-background px-4 dark:bg-2x-dark-foreground': paypal.isMethodsLoaded,
}"
>
<PaymentMethod
@click.native="payByPayPal"
driver="paypal"
:description="config.paypal_payment_description"
>
<div v-if="paypal.isMethodLoading" class="translate-y-3 scale-50 transform">
<Spinner />
</div>
<span
v-if="!paypal.isMethodsLoaded"
:class="{ 'opacity-0': paypal.isMethodLoading }"
class="text-theme cursor-pointer text-sm font-bold"
>
{{ $t('select') }}
</span>
</PaymentMethod>
<!--PayPal Buttons-->
<div id="paypal-button-container"></div>
</div>
<!--Paystack implementation-->
<PaymentMethod
v-if="config.isPaystack"
driver="paystack"
:description="$t(config.paystack_payment_description)"
>
<div v-if="paystack.isGettingCheckoutLink" class="translate-y-3 scale-50 transform">
<Spinner />
</div>
<span
@click="payByPaystack()"
:class="{ 'opacity-0': paystack.isGettingCheckoutLink }"
class="text-theme cursor-pointer text-sm font-bold"
>
{{ $t('select') }}
</span>
</PaymentMethod>
</PopupContent>
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary">
{{ $t('cancel_payment') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import PopupWrapper from '../../Popups/Components/PopupWrapper'
import PopupActions from '../../Popups/Components/PopupActions'
import PopupContent from '../../Popups/Components/PopupContent'
import PopupHeader from '../../Popups/Components/PopupHeader'
import ButtonBase from '../../UI/Buttons/ButtonBase'
import { loadScript } from '@paypal/paypal-js'
import PaymentMethod from '../PaymentMethod'
import Spinner from '../../UI/Others/Spinner'
import InfoBox from "../../UI/Others/InfoBox"
import { events } from '../../../bus'
import { mapGetters } from 'vuex'
import axios from "axios";
export default {
name: 'ChargePaymentPopup',
components: {
PaymentMethod,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
Spinner,
InfoBox,
},
data() {
return {
paypal: {
isMethodsLoaded: false,
isMethodLoading: false,
},
paystack: {
isGettingCheckoutLink: false,
},
}
},
computed: {
...mapGetters(['singleChargeAmount', 'config', 'user']),
},
methods: {
payByPaystack() {
this.paystack.isGettingCheckoutLink = true
axios
.post('/api/paystack/checkout', {
amount: this.singleChargeAmount * 100,
})
.then((response) => {
window.location = response.data.data.authorization_url
})
},
async payByPayPal() {
if (this.paypal.isMethodLoading) {
return
}
this.paypal.isMethodLoading = true
let paypal
try {
paypal = await loadScript({
'client-id': this.config.paypal_client_id,
vault: true,
})
} catch (error) {
events.$emit('toaster', {
type: 'danger',
message: this.$t('failed_to_load_paypal'),
})
}
const userId = this.user.data.id
const amount = this.singleChargeAmount
this.paypal.isMethodsLoaded = true
this.paypal.isMethodLoading = false
const app = this
// Initialize paypal buttons for single charge
await paypal
.Buttons({
createOrder: function (data, actions) {
return actions.order.create({
purchase_units: [
{
amount: {
value: amount,
},
custom_id: userId,
},
],
})
},
onApprove: function () {
app.paymentSuccessful()
},
})
.render('#paypal-button-container')
},
paymentSuccessful() {
this.$closePopup()
events.$emit('toaster', {
type: 'success',
message: this.$t('payment_was_successfully_received'),
})
// todo: temporary reload function
setTimeout(() => document.location.reload(), 500)
},
},
created() {
events.$on('popup:close', () => (this.paypal.isMethodsLoaded = false))
},
}
</script>
@@ -1,312 +0,0 @@
<template>
<PopupWrapper name="select-plan-subscription">
<PopupHeader :title="$t('upgrade_your_account')" icon="credit-card" />
<!--Payment Options-->
<div v-if="isPaymentOptionPage">
<PopupContent>
<!--Stripe implementation-->
<PaymentMethod
v-if="config.isStripe"
driver="stripe"
:description="$t(config.stripe_payment_description)"
>
<div v-if="stripe.isGettingCheckoutLink" class="translate-y-3 scale-50 transform">
<Spinner />
</div>
<span
@click="payByStripe"
:class="{ 'opacity-0': stripe.isGettingCheckoutLink }"
class="text-theme cursor-pointer text-sm font-bold"
>
{{ $t('select') }}
</span>
</PaymentMethod>
<!--PayPal implementation-->
<div
v-if="config.isPayPal"
:class="{
'mb-2 rounded-xl bg-light-background px-4 dark:bg-2x-dark-foreground': paypal.isMethodsLoaded,
}"
>
<PaymentMethod
@click.native="payByPayPal"
driver="paypal"
:description="$t(config.paypal_payment_description)"
>
<div v-if="paypal.isMethodLoading" class="translate-y-3 scale-50 transform">
<Spinner />
</div>
<span
v-if="!paypal.isMethodsLoaded"
:class="{ 'opacity-0': paypal.isMethodLoading }"
class="text-theme cursor-pointer text-sm font-bold"
>
{{ $t('select') }}
</span>
</PaymentMethod>
<!--PayPal Buttons-->
<div id="paypal-button-container"></div>
</div>
<!--Paystack implementation-->
<PaymentMethod
v-if="config.isPaystack"
driver="paystack"
:description="$t(config.paystack_payment_description)"
>
<div v-if="paystack.isGettingCheckoutLink" class="translate-y-3 scale-50 transform">
<Spinner />
</div>
<span
@click="payByPaystack()"
:class="{ 'opacity-0': paystack.isGettingCheckoutLink }"
class="text-theme cursor-pointer text-sm font-bold"
>
{{ $t('select') }}
</span>
</PaymentMethod>
</PopupContent>
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary">
{{ $t('cancel_payment') }}
</ButtonBase>
</PopupActions>
</div>
<!--Select Payment Plans-->
<div v-if="!isPaymentOptionPage">
<PopupContent v-if="plans">
<InfoBox v-if="plans.data.length === 0" class="!mb-0">
<p>There isn't any plan yet.</p>
</InfoBox>
<PlanPeriodSwitcher v-if="yearlyPlans.length > 0" v-model="isSelectedYearlyPlans" />
<!--List available plans-->
<div>
<PlanDetail
v-for="(plan, i) in plans.data"
:plan="plan"
:key="plan.data.id"
v-if="plan.data.attributes.interval === intervalPlanType"
:class="{ 'pointer-events-none opacity-50': userSubscribedPlanId === plan.data.id }"
:is-selected="selectedPlan && selectedPlan.data.id === plan.data.id"
@click.native="selectPlan(plan)"
/>
</div>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary"
>{{ $t('cancel') }}
</ButtonBase>
<ButtonBase
class="w-full"
v-if="plans && plans.data.length !== 0"
:button-style="buttonStyle"
@click.native="isPaymentOptionPage = true"
>{{ $t('upgrade_account') }}
</ButtonBase>
</PopupActions>
</div>
</PopupWrapper>
</template>
<script>
import PaymentMethod from '../PaymentMethod'
import { loadScript } from '@paypal/paypal-js'
import SwitchInput from '../../Inputs/SwitchInput'
import PopupWrapper from '../../Popups/Components/PopupWrapper'
import PopupActions from '../../Popups/Components/PopupActions'
import PopupContent from '../../Popups/Components/PopupContent'
import PopupHeader from '../../Popups/Components/PopupHeader'
import ButtonBase from '../../UI/Buttons/ButtonBase'
import PlanDetail from '../PlanDetail'
import { mapGetters } from 'vuex'
import { events } from '../../../bus'
import axios from 'axios'
import Spinner from '../../UI/Others/Spinner'
import InfoBox from '../../UI/Others/InfoBox'
import PlanPeriodSwitcher from '../PlanPeriodSwitcher'
export default {
name: 'SubscribeAccountPopup',
components: {
PlanPeriodSwitcher,
InfoBox,
Spinner,
PaymentMethod,
PlanDetail,
SwitchInput,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
},
watch: {
isSelectedYearlyPlans() {
this.selectedPlan = undefined
},
},
computed: {
...mapGetters(['config', 'user']),
intervalPlanType() {
return this.isSelectedYearlyPlans ? 'year' : 'month'
},
buttonStyle() {
return this.selectedPlan ? 'theme' : 'secondary'
},
userSubscribedPlanId() {
return (
this.user &&
this.user.data.relationships.subscription &&
this.user.data.relationships.subscription.data.relationships.plan.data.id
)
},
yearlyPlans() {
return this.plans.data.filter((plan) => plan.data.attributes.interval === 'year')
},
},
data() {
return {
paystack: {
isGettingCheckoutLink: false,
},
stripe: {
isGettingCheckoutLink: false,
},
paypal: {
isMethodsLoaded: false,
isMethodLoading: false,
},
isPaymentOptionPage: false,
isSelectedYearlyPlans: false,
isLoading: false,
selectedPlan: undefined,
plans: undefined,
}
},
methods: {
payByPaystack() {
this.paystack.isGettingCheckoutLink = true
axios
.post('/api/paystack/checkout', {
planCode: this.selectedPlan.data.meta.driver_plan_id.paystack,
})
.then((response) => {
window.location = response.data.data.authorization_url
})
},
async payByPayPal() {
if (this.paypal.isMethodLoading) {
return
}
this.paypal.isMethodLoading = true
let paypal
try {
paypal = await loadScript({
'client-id': this.config.paypal_client_id,
vault: true,
})
} catch (error) {
events.$emit('toaster', {
type: 'danger',
message: this.$t('failed_to_load_paypal'),
})
}
const planId = this.selectedPlan.data.meta.driver_plan_id.paypal
const userId = this.user.data.id
const app = this
this.paypal.isMethodsLoaded = true
this.paypal.isMethodLoading = false
// Initialize paypal buttons for single charge
await paypal
.Buttons({
createSubscription: function (data, actions) {
return actions.subscription.create({
plan_id: planId,
custom_id: userId,
})
},
onApprove: function () {
app.paymentSuccessful()
},
})
.render('#paypal-button-container')
},
payByStripe() {
this.stripe.isGettingCheckoutLink = true
axios
.post('/api/stripe/checkout', {
planCode: this.selectedPlan.data.meta.driver_plan_id.stripe,
})
.then((response) => {
window.location = response.data.url
})
.catch((error) => {
this.$closePopup()
setTimeout(() => {
if (error.response.status === 500 && error.response.data.type) {
events.$emit('alert:open', {
title: error.response.data.title,
message: error.response.data.message,
})
} else {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
}
}, 100)
})
.finally(() => {
this.stripe.isGettingCheckoutLink = false
})
},
selectPlan(plan) {
this.selectedPlan = plan
},
paymentSuccessful() {
this.$closePopup()
events.$emit('toaster', {
type: 'success',
message: this.$t('payment_was_successfully_received'),
})
// todo: temporary reload function
setTimeout(() => document.location.reload(), 1000)
},
},
created() {
// Load available plans
axios.get('/api/subscriptions/plans').then((response) => {
this.plans = response.data
})
// Reset states on popup close
events.$on('popup:close', () => {
this.isSelectedYearlyPlans = false
this.isPaymentOptionPage = false
this.selectedPlan = undefined
this.paypal.isMethodsLoaded = false
})
},
}
</script>
@@ -1,12 +0,0 @@
<template>
<div v-if="$store.getters.isLimitedUser" class="bg-red-500 py-1 text-center">
<router-link :to="{ name: 'Billing' }" class="text-xs font-bold text-white">
{{ $t('restricted_account_warning') }}
</router-link>
</div>
</template>
<script>
export default {
name: 'RestrictionWarningBar',
}
</script>
@@ -1,91 +0,0 @@
<template>
<div v-if="!hasPaymentMethod" class="card shadow-card">
<FormLabel icon="dollar">
{{ $t('balance') }}
</FormLabel>
<b class="-mt-3 mb-0.5 block text-2xl font-extrabold sm:text-3xl">
{{ user.data.relationships.balance.data.attributes.formatted }}
</b>
<!-- Make payment form -->
<ValidationObserver
ref="fundAccount"
@submit.prevent="makePayment"
v-slot="{ invalid }"
tag="form"
class="mt-6"
>
<ValidationProvider
tag="div"
v-slot="{ errors }"
mode="passive"
name="Amount"
:rules="`required|min_value:${user.data.meta.totalDebt.amount}`"
>
<AppInputText
:description="
$t('amount_increase_description')
"
:error="errors[0]"
:is-last="true"
>
<div class="space-y-4 sm:flex sm:space-x-4 sm:space-y-0">
<input
v-model="chargeAmount"
:placeholder="$t('fund_account_balance')"
type="number"
min="1"
max="999999999"
class="focus-border-theme input-dark"
:class="{ '!border-rose-600': errors[0] }"
/>
<ButtonBase type="submit" button-style="theme" class="w-full sm:w-auto">
{{ $t('make_payment') }}
</ButtonBase>
</div>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</div>
</template>
<script>
import { ValidationObserver, ValidationProvider } from 'vee-validate/dist/vee-validate.full'
import ButtonBase from '../UI/Buttons/ButtonBase'
import FormLabel from '../UI/Labels/FormLabel'
import AppInputText from '../Forms/Layouts/AppInputText'
import { mapGetters } from 'vuex'
export default {
name: 'UserBalance',
components: {
ValidationObserver,
ValidationProvider,
AppInputText,
ButtonBase,
FormLabel,
},
computed: {
...mapGetters(['user']),
hasPaymentMethod() {
return this.user.data.relationships.creditCards && this.user.data.relationships.creditCards.data.length > 0
},
},
data() {
return {
chargeAmount: undefined,
}
},
methods: {
async makePayment() {
// Validate fields
const isValid = await this.$refs.fundAccount.validate()
if (!isValid) return
// Show payment methods popup
this.$store.dispatch('callSingleChargeProcess', this.chargeAmount)
},
},
}
</script>
@@ -1,244 +0,0 @@
<template>
<div class="card shadow-card">
<FormLabel icon="bell">
{{ $t('billing_alert') }}
</FormLabel>
<div v-if="user.data.relationships.alert">
<b class="-mt-3 mb-0.5 block flex items-center text-2xl font-extrabold sm:text-3xl">
{{ user.data.relationships.alert.data.attributes.formatted }}
<edit2-icon
v-if="!showUpdateBillingAlertForm"
@click="showUpdateBillingAlertForm = !showUpdateBillingAlertForm"
size="12"
class="vue-feather ml-2 -translate-y-0.5 transform cursor-pointer"
/>
<trash2-icon
v-if="showUpdateBillingAlertForm"
@click="deleteBillingAlert"
size="12"
class="vue-feather ml-2 -translate-y-0.5 transform cursor-pointer"
/>
</b>
<b class="block text-sm dark:text-gray-500 text-gray-400">
{{ $t('billing_alert_description') }}
</b>
</div>
<ValidationObserver
v-if="showUpdateBillingAlertForm"
ref="updatebillingAlertForm"
@submit.prevent="updateBillingAlert"
v-slot="{ invalid }"
tag="form"
class="mt-6"
>
<ValidationProvider tag="div" v-slot="{ errors }" mode="passive" name="Billing Alert" rules="required">
<AppInputText
:description="
$t(
'billing_alert_notes'
)
"
:error="errors[0]"
:is-last="true"
>
<div class="space-y-4 sm:flex sm:space-x-4 sm:space-y-0">
<input
v-model="billingAlertAmount"
:placeholder="$t('alert_amount_')"
type="number"
min="1"
max="999999999"
class="focus-border-theme input-dark"
:class="{ '!border-rose-600': errors[0] }"
/>
<ButtonBase
:loadint="isSendingBillingAlert"
:disabled="isSendingBillingAlert"
type="submit"
button-style="theme"
class="w-full sm:w-auto"
>
{{ $t('update_alert') }}
</ButtonBase>
</div>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
<ValidationObserver
v-if="!user.data.relationships.alert"
ref="billingAlertForm"
@submit.prevent="setBillingAlert"
v-slot="{ invalid }"
tag="form"
class="mt-6"
>
<ValidationProvider tag="div" v-slot="{ errors }" mode="passive" name="Billing Alert" rules="required">
<AppInputText
:description="
$t(
'billing_alert_notes'
)
"
:error="errors[0]"
:is-last="true"
>
<div class="space-y-4 sm:flex sm:space-x-4 sm:space-y-0">
<input
v-model="billingAlertAmount"
:placeholder="$t('alert_amount_')"
type="number"
min="1"
max="999999999"
class="focus-border-theme input-dark"
:class="{ '!border-rose-600': errors[0] }"
/>
<ButtonBase
:loadint="isSendingBillingAlert"
:disabled="isSendingBillingAlert"
type="submit"
button-style="theme"
class="w-full sm:w-auto"
>
{{ $t('set_alert') }}
</ButtonBase>
</div>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
</div>
</template>
<script>
import { ValidationObserver, ValidationProvider } from 'vee-validate/dist/vee-validate.full'
import ButtonBase from '../UI/Buttons/ButtonBase'
import AppInputText from '../Forms/Layouts/AppInputText'
import FormLabel from '../UI/Labels/FormLabel'
import { Edit2Icon, Trash2Icon } from 'vue-feather-icons'
import { events } from '../../bus'
import { mapGetters } from 'vuex'
import axios from 'axios'
export default {
name: 'UserBillingAlerts',
components: {
ValidationObserver,
ValidationProvider,
AppInputText,
ButtonBase,
Trash2Icon,
Edit2Icon,
FormLabel,
},
computed: {
...mapGetters(['user']),
},
data() {
return {
billingAlertAmount: undefined,
isSendingBillingAlert: false,
showUpdateBillingAlertForm: false,
}
},
methods: {
async updateBillingAlert() {
// Validate fields
const isValid = await this.$refs.updatebillingAlertForm.validate()
if (!isValid) return
this.isSendingBillingAlert = true
axios
.patch(`/api/subscriptions/billing-alerts/${this.user.data.relationships.alert.data.id}`, {
amount: this.billingAlertAmount,
})
.then(() => {
this.$store.dispatch('getAppData')
this.showUpdateBillingAlertForm = false
events.$emit('toaster', {
type: 'success',
message: this.$t('alert_updated'),
})
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.isSendingBillingAlert = false
})
},
async setBillingAlert() {
// Validate fields
const isValid = await this.$refs.billingAlertForm.validate()
if (!isValid) return
this.isSendingBillingAlert = true
axios
.post('/api/subscriptions/billing-alerts', {
amount: this.billingAlertAmount,
})
.then(() => {
this.$store.dispatch('getAppData')
events.$emit('toaster', {
type: 'success',
message: this.$t('alert_set_successfully'),
})
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.isSendingBillingAlert = false
})
},
deleteBillingAlert() {
events.$emit('confirm:open', {
title: this.$t('want_to_delete_alert'),
message: this.$t(
'want_to_delete_alert_description'
),
action: {
id: this.user.data.relationships.alert.data.id,
operation: 'delete-billing-alert',
},
})
},
},
created() {
events.$on('action:confirmed', (data) => {
if (data.operation === 'delete-billing-alert')
axios
.delete(`/api/subscriptions/billing-alerts/${this.user.data.relationships.alert.data.id}`)
.then(() => {
this.$store.dispatch('getAppData')
this.showUpdateBillingAlertForm = false
this.billingAlertAmount = undefined
events.$emit('toaster', {
type: 'success',
message: this.$t('deleted_alert'),
})
})
.catch(() => this.$isSomethingWrong())
})
},
destroyed() {
events.$off('action:confirmed')
},
}
</script>
@@ -1,116 +0,0 @@
<template>
<div v-if="hasSubscription" class="card shadow-card">
<FormLabel>
{{ $t('edit_your_subscription') }}
</FormLabel>
<AppInputButton
v-if="subscription.attributes.status !== 'cancelled'"
:title="$t('cancel_subscription')"
:description="
$t(
'cancel_subscription_description'
)
"
>
<ButtonBase
@click.native="cancelSubscriptionConfirmation"
:loading="isCancelling"
class="w-full sm:w-auto"
button-style="secondary"
>
{{ $t('cancel_now') }}
</ButtonBase>
</AppInputButton>
<AppInputButton
:title="$t('upgrade_downgrade_plan')"
:description="$t('upgrade_downgrade_plan_description')"
:is-last="true"
>
<ButtonBase @click.native="$changeSubscriptionOptions" class="w-full sm:w-auto" button-style="secondary">
{{ $t('change_plan') }}
</ButtonBase>
</AppInputButton>
</div>
</template>
<script>
import AppInputButton from '../Forms/Layouts/AppInputButton'
import AppInputText from '../Forms/Layouts/AppInputText'
import AppInputSwitch from '../Forms/Layouts/AppInputSwitch'
import ButtonBase from '../UI/Buttons/ButtonBase'
import FormLabel from '../UI/Labels/FormLabel'
import { events } from '../../bus'
import axios from 'axios'
export default {
name: 'UserEditSubscription',
components: {
AppInputButton,
AppInputSwitch,
AppInputText,
ButtonBase,
FormLabel,
},
computed: {
subscription() {
return this.$store.getters.user.data.relationships.subscription.data
},
hasSubscription() {
return this.$store.getters.user.data.relationships.subscription
},
},
data() {
return {
isCancelling: false,
}
},
methods: {
cancelSubscriptionConfirmation() {
events.$emit('confirm:open', {
title: this.$t('want_cancel_subscription'),
message: this.$t(
"popup_subscription_cancel.message"
),
action: {
operation: 'cancel-subscription',
},
})
},
},
created() {
events.$on('action:confirmed', (data) => {
if (data.operation === 'cancel-subscription') {
// Start deleting spinner button
this.isCancelling = true
// Send post request
axios
.post('/api/subscriptions/cancel')
.then(() => {
// Update user data
this.$store.dispatch('getAppData')
events.$emit('toaster', {
type: 'success',
message: this.$t('popup_subscription_cancel.title'),
})
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.isCancelling = false
})
}
})
},
destroyed() {
events.$off('action:confirmed')
},
}
</script>
@@ -1,45 +0,0 @@
<template>
<div v-if="!hasSubscription" class="card shadow-card">
<FormLabel>
{{ $t('subscription') }}
</FormLabel>
<b class="-mt-3 mb-0.5 block text-2xl font-extrabold sm:text-3xl">
{{ $t('free_plan') }}
</b>
<b v-if="$store.getters.config.allowed_payments" class="mb-3 mb-8 block text-sm text-gray-400">
{{ $t('upgrade_to_get_more') }}
</b>
<ButtonBase
v-if="$store.getters.config.allowed_payments"
@click.native="$openSubscribeOptions"
type="submit"
button-style="theme"
class="mt-4 w-full"
>
{{ $t('upgrade_your_account') }}
</ButtonBase>
</div>
</template>
<script>
import InfoBox from '../UI/Others/InfoBox'
import FormLabel from '../UI/Labels/FormLabel'
import ButtonBase from '../UI/Buttons/ButtonBase'
export default {
name: 'UserEmptySubscription',
components: {
ButtonBase,
FormLabel,
InfoBox,
},
computed: {
hasSubscription() {
return this.$store.getters.user.data.relationships.subscription
},
},
}
</script>
@@ -1,67 +0,0 @@
<template>
<div
v-if="user.data.relationships.failedPayments && user.data.relationships.failedPayments.data.length > 0"
class="card shadow-card"
>
<FormLabel icon="frown">
{{ $t('failed_payments') }}
</FormLabel>
<b class="-mt-3 mb-0.5 block text-2xl font-extrabold sm:text-3xl">
-{{ user.data.meta.totalDebt.formatted }}
</b>
<b class="mb-3 mb-5 block text-sm text-gray-400">
{{
$t(
"unable_to_charge"
)
}}
</b>
<!--Failed Payments-->
<div
v-for="payment in user.data.relationships.failedPayments.data"
:key="payment.data.id"
class="flex items-center justify-between border-b border-dashed border-light py-2 dark:border-opacity-5"
>
<div class="w-2/4 leading-none">
<b class="text-sm font-bold leading-none">
{{ payment.data.attributes.note }}
</b>
</div>
<div class="w-1/4 text-left">
<span class="text-gray-560 text-sm font-bold capitalize">
{{ $t(payment.data.attributes.source) }}
</span>
</div>
<div class="w-1/4 text-right">
<span class="text-sm font-bold">
{{ payment.data.attributes.created_at }}
</span>
</div>
<div class="w-1/4 text-right">
<span class="text-red text-sm font-bold">
{{ payment.data.attributes.amount }}
</span>
</div>
</div>
</div>
</template>
<script>
import FormLabel from '../UI/Labels/FormLabel'
import InfoBox from '../UI/Others/InfoBox'
import { mapGetters } from 'vuex'
export default {
name: 'UserFailedPayments',
components: {
FormLabel,
InfoBox,
},
computed: {
...mapGetters(['user']),
},
}
</script>
@@ -1,93 +0,0 @@
<template>
<div v-if="hasSubscription" class="card shadow-card">
<FormLabel>
{{ $t('subscription') }}
</FormLabel>
<b class="-mt-3 mb-0.5 block text-xl font-extrabold sm:text-3xl">
{{ status }}
</b>
<b class="mb-3 mb-8 block text-sm dark:text-gray-500 text-gray-400">
{{ subscription.data.relationships.plan.data.attributes.name }} /
{{ price }}
</b>
<div v-for="(limit, i) in limitations" :key="i" :class="{ 'mb-6': Object.keys(limitations).length - 1 !== i }">
<b class="mb-3 block text-sm dark:text-gray-500 text-gray-400">
{{ limit.message }}
</b>
<ProgressLine v-if="limit.isVisibleBar" :data="limit.distribution" />
</div>
</div>
</template>
<script>
import FormLabel from '../UI/Labels/FormLabel'
import ProgressLine from '../UI/ProgressChart/ProgressLine'
import { mapGetters } from 'vuex'
export default {
name: 'UserFixedSubscriptionDetail',
components: {
ProgressLine,
FormLabel,
},
computed: {
...mapGetters(['user']),
subscription() {
return this.$store.getters.user.data.relationships.subscription
},
hasSubscription() {
return this.$store.getters.user.data.relationships.subscription
},
limitations() {
let limitations = []
Object.entries(this.user.data.meta.limitations).map(([key, item]) => {
let payload = {
color: {
max_storage_amount: 'warning',
max_team_members: 'purple',
},
message: {
max_storage_amount: this.$t('total_x_of_x_used', {use: item.use, total:item.total }),
max_team_members: item.total === -1
? this.$t('max_team_members.unlimited')
: this.$t('total_x_of_x_members', {use: item.use, total:item.total }),
},
title: {
max_storage_amount: this.$t('storage'),
max_team_members: this.$t('team_members'),
},
}
limitations.push({
message: payload.message[key],
isVisibleBar: item.total !== -1,
distribution: [
{
progress: item.percentage,
color: payload.color[key],
title: payload.title[key],
},
],
})
})
return limitations
},
status() {
return {
active: this.$t('active_until', {date: this.subscription.data.attributes.renews_at}),
cancelled: this.$t('ends_at_date', {date: this.subscription.data.attributes.ends_at}),
}[this.subscription.data.attributes.status]
},
price() {
return {
month: this.$t('price_per_month', {price: this.subscription.data.relationships.plan.data.attributes.price}),
year: this.$t('price_per_year', {price: this.subscription.data.relationships.plan.data.attributes.price}),
}[this.subscription.data.relationships.plan.data.attributes.interval]
},
},
}
</script>
@@ -1,216 +0,0 @@
<template>
<div v-if="canShowForMeteredBilling || canShowForFixedBilling" class="card shadow-card">
<FormLabel icon="credit-card">
{{ $t('payment_method') }}
</FormLabel>
<!-- User has registered payment method -->
<div v-if="hasPaymentMethod">
<b
v-if="
config.subscriptionType === 'metered' && user.data.relationships.balance.data.attributes.balance > 0
"
class="mb-3 mb-5 block text-sm"
>
{{
$t('credit_to_auto_withdraw', {
credit: user.data.relationships.balance.data.attributes.formatted,
})
}}
</b>
<!-- Card -->
<PaymentCard v-for="card in user.data.relationships.creditCards.data" :key="card.data.id" :card="card" />
<small class="hidden pt-3 text-xs leading-none dark:text-gray-500 text-gray-500 sm:block">
{{ $t('auto_settled_credit_card') }}
</small>
</div>
<!-- User doesn't have registered payment method -->
<div v-if="!hasPaymentMethod">
<!-- Show credit card form -->
<ButtonBase
@click.native="showStoreCreditCardForm"
v-if="!isCreditCardForm"
:loading="stripe.storingStripePaymentMethod"
type="submit"
button-style="theme"
class="mt-4 w-full"
>
{{ $t('add_payment_method') }}
</ButtonBase>
<!-- Store credit card form -->
<form v-if="isCreditCardForm" @submit.prevent="storeStripePaymentMethod" id="payment-form" class="mt-6">
<div v-if="stripe.isInitialization" class="relative mb-6 h-10">
<Spinner />
</div>
<InfoBox v-if="config.isDemo && !stripe.isInitialization">
<p>For adding test credit card please use <b class="text-theme">4242 4242 4242 4242</b> as a card number, <b class="text-theme">11/22</b>
as the expiration date and <b class="text-theme">123</b> as CVC number and ZIP <b class="text-theme">12345</b> if required.</p>
</InfoBox>
<div id="payment-element">
<!-- Elements will create form elements here -->
</div>
<ButtonBase
:loading="stripe.storingStripePaymentMethod"
type="submit"
button-style="theme"
class="mt-4 w-full"
>
{{ $t('store_my_credit_card') }}
</ButtonBase>
<div id="error-message" class="pt-2 text-xs text-rose-600">
<!-- Display error message to your customers here -->
</div>
</form>
</div>
</div>
</template>
<script>
import ButtonBase from '../UI/Buttons/ButtonBase'
import FormLabel from '../UI/Labels/FormLabel'
import PaymentCard from './PaymentCard'
import Spinner from '../UI/Others/Spinner'
import { mapGetters } from 'vuex'
import { events } from '../../bus'
import { loadStripe } from '@stripe/stripe-js'
import axios from 'axios'
import InfoBox from "../UI/Others/InfoBox";
// Define stripe variables
let stripe,
elements = undefined
export default {
name: 'UserStoredPaymentMethods',
components: {
InfoBox,
ButtonBase,
FormLabel,
PaymentCard,
Spinner,
},
computed: {
...mapGetters(['isDarkMode', 'config', 'user']),
canShowForMeteredBilling() {
return this.config.isStripe && this.config.subscriptionType === 'metered'
},
canShowForFixedBilling() {
return (
this.config.isStripe &&
this.config.subscriptionType === 'fixed' &&
this.$store.getters.user.data.relationships.subscription &&
this.$store.getters.user.data.relationships.subscription.data.attributes.driver === 'stripe'
)
},
hasPaymentMethod() {
return this.user.data.relationships.creditCards && this.user.data.relationships.creditCards.data.length > 0
},
},
data() {
return {
isLoading: false,
isCreditCardForm: false,
stripe: {
isInitialization: true,
storingStripePaymentMethod: false,
},
}
},
methods: {
async storeStripePaymentMethod() {
if (this.config.isDemo && this.user.data.attributes.email === 'ho**@hi5ve.digital') {
events.$emit('toaster', {
type: 'success',
message: this.$t('credit_card_stored'),
})
return
}
this.stripe.storingStripePaymentMethod = true
const { error } = await stripe.confirmSetup({
//`Elements` instance that was used to create the Payment Element
elements,
redirect: 'if_required',
confirmParams: {
return_url: window.location.href,
},
})
if (error) {
// This point will only be reached if there is an immediate error when
// confirming the payment. Show error to your customer (e.g., payment
// details incomplete)
const messageContainer = document.querySelector('#error-message')
messageContainer.textContent = error.message
} else {
// Your customer will be redirected to your `return_url`. For some payment
// methods like iDEAL, your customer will be redirected to an intermediate
// site first to authorize the payment, then redirected to the `return_url`.
events.$emit('toaster', {
type: 'success',
message: this.$t('credit_card_stored'),
})
// TODO: L9 - load credit card after was stored in database
setTimeout(() => document.location.reload(), 500)
}
this.stripe.storingStripePaymentMethod = false
},
async stripeInit() {
// Init stripe js
stripe = await loadStripe(this.config.stripe_public_key)
await axios
.get('/api/stripe/setup-intent')
.then((response) => {
// Set up Stripe.js and Elements to use in checkout form, passing the client secret obtained in step 2
elements = stripe.elements({
clientSecret: response.data.client_secret,
appearance: {
theme: 'stripe',
variables: {
colorPrimary: this.config.app_color,
fontFamily: 'Nunito',
borderRadius: '8px',
colorText: this.isDarkMode ? '#bec6cf' : '#1B2539',
colorBackground: this.isDarkMode ? '#191b1e' : '#fff',
fontWeightNormal: '700',
fontSizeSm: '0.875rem',
colorSuccessText: '#0ABB87',
colorSuccess: '#0ABB87',
colorWarning: '#fd397a',
colorWarningText: '#fd397a',
colorDangerText: '#fd397a',
colorTextSecondary: '#6b7280',
spacingGridRow: '20px',
},
},
})
// Create and mount the Payment Element
const paymentElement = elements.create('payment')
paymentElement.mount('#payment-element')
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
this.stripe.isInitialization = false
},
showStoreCreditCardForm() {
this.isCreditCardForm = !this.isCreditCardForm
this.stripeInit()
},
}
}
</script>
@@ -1,50 +0,0 @@
<template>
<div class="card shadow-card">
<FormLabel icon="file-text">
{{ $t('transactions') }}
</FormLabel>
<DatatableWrapper
class="overflow-x-auto"
api="/api/subscriptions/transactions"
:paginator="true"
:columns="columns"
>
<template slot-scope="{ row }">
<FixedTransactionRow :row="row" />
</template>
<!--Empty page-->
<template v-slot:empty-page>
<InfoBox style="margin-bottom: 0">
<p>{{ $t('user_invoices.empty') }}</p>
</InfoBox>
</template>
</DatatableWrapper>
</div>
</template>
<script>
import InfoBox from '../UI/Others/InfoBox'
import DatatableWrapper from '../UI/Table/DatatableWrapper'
import FixedTransactionRow from './FixedTransactionRow'
import FormLabel from '../UI/Labels/FormLabel'
import ColorLabel from '../UI/Labels/ColorLabel'
export default {
name: 'UserTransactionsForFixedBilling',
components: {
FixedTransactionRow,
DatatableWrapper,
ColorLabel,
FormLabel,
InfoBox,
},
computed: {
columns() {
return this.$store.getters.transactionColumns.filter(
(column) => !['type', 'user_id'].includes(column.field)
)
},
},
}
</script>
@@ -1,68 +0,0 @@
<template>
<div class="card shadow-card">
<FormLabel icon="file-text">
{{ $t('transactions') }}
</FormLabel>
<DatatableWrapper class="overflow-x-auto" api="/api/user/transactions" :paginator="true" :columns="columns">
<template slot-scope="{ row }">
<!--Transaction rows-->
<MeteredTransactionRow :row="row" @showDetail="showTransactionDetail" />
<!--Transaction detail-->
<MeteredTransactionDetailRow
v-if="row.data.attributes.metadata && showedTransactionDetailById === row.data.id"
:row="row"
/>
</template>
<!--Empty page-->
<template v-slot:empty-page>
<InfoBox style="margin-bottom: 0">
<p>{{ $t('user_invoices.empty') }}</p>
</InfoBox>
</template>
</DatatableWrapper>
</div>
</template>
<script>
import { EyeIcon, FileTextIcon } from 'vue-feather-icons'
import ColorLabel from '../UI/Labels/ColorLabel'
import DatatableWrapper from '../UI/Table/DatatableWrapper'
import FormLabel from '../UI/Labels/FormLabel'
import InfoBox from '../UI/Others/InfoBox'
import { mapGetters } from 'vuex'
import MeteredTransactionDetailRow from './MeteredTransactionDetailRow'
import MeteredTransactionRow from './MeteredTransactionRow'
export default {
name: 'UserTransactionsForMeteredBilling',
components: {
MeteredTransactionDetailRow,
MeteredTransactionRow,
DatatableWrapper,
ColorLabel,
FormLabel,
InfoBox,
FileTextIcon,
EyeIcon,
},
computed: {
...mapGetters(['user']),
columns() {
return this.$store.getters.transactionColumns.filter((column) => column.field !== 'user_id')
},
},
data() {
return {
showedTransactionDetailById: undefined,
}
},
methods: {
showTransactionDetail(id) {
if (this.showedTransactionDetailById === id) this.showedTransactionDetailById = undefined
else this.showedTransactionDetailById = id
},
},
}
</script>
@@ -1,80 +0,0 @@
<template>
<div v-if="canShowForSubscription" class="card shadow-card">
<FormLabel>
{{ $t('update_payments') }}
</FormLabel>
<AppInputButton
:title="$t('update_payment_method')"
:description="$t('payment_method_update_redirect_description')"
:is-last="true"
>
<ButtonBase
@click.native="updatePaymentMethod"
:loading="isGeneratedUpdateLink"
class="w-full sm:w-auto"
button-style="theme"
>
{{ $t('update_payments') }}
</ButtonBase>
</AppInputButton>
</div>
</template>
<script>
import AppInputSwitch from '../Forms/Layouts/AppInputSwitch'
import ButtonBase from '../UI/Buttons/ButtonBase'
import FormLabel from '../UI/Labels/FormLabel'
import axios from 'axios'
import { events } from '../../bus'
import AppInputButton from '../Forms/Layouts/AppInputButton'
export default {
name: 'UserUpdatePaymentMethodsExternally',
components: {
AppInputButton,
AppInputSwitch,
ButtonBase,
FormLabel,
},
computed: {
canShowForSubscription() {
return (
this.hasSubscription &&
!this.subscription.attributes.is_cancelled &&
['paystack', 'paypal'].includes(this.subscription.attributes.driver)
)
},
subscription() {
return this.$store.getters.user.data.relationships.subscription.data
},
hasSubscription() {
return this.$store.getters.user.data.relationships.subscription
},
},
data() {
return {
isGeneratedUpdateLink: false,
}
},
methods: {
updatePaymentMethod() {
this.isGeneratedUpdateLink = true
axios
.post(`/api/subscriptions/edit/${this.subscription.id}`)
.then((response) => {
window.location = response.data.url
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
this.isGeneratedUpdateLink = false
})
},
},
}
</script>
@@ -1,59 +0,0 @@
<template>
<div class="card shadow-card">
<FormLabel icon="bar-chart">
{{ $t('usage_estimates') }}
</FormLabel>
<b class="-mt-3 mb-0.5 block text-2xl font-extrabold sm:text-3xl">
{{ user.data.meta.usages.costEstimate }}
</b>
<b class="mb-3 mb-5 block text-sm dark:text-gray-500 text-gray-400">
{{ user.data.relationships.subscription.data.attributes.updated_at }}
{{ $t('till_now') }}
</b>
<div
class="flex items-center justify-between border-b border-dashed border-light py-2 dark:border-opacity-5"
v-for="(usage, i) in user.data.meta.usages.featureEstimates"
:key="i"
>
<div class="w-2/4 leading-none">
<b class="text-sm font-bold leading-none">
{{ $t(usage.feature) }}
</b>
<small class="hidden pt-2 text-xs leading-none dark:text-gray-500 text-gray-500 sm:block">
{{ $t(`feature_usage_desc_${usage.feature}`) }}
</small>
</div>
<div class="w-1/4 text-left">
<span class="text-gray-560 text-sm font-bold">
{{ usage.usage }}
</span>
</div>
<div class="w-1/4 text-right">
<span class="text-theme text-sm font-bold">
{{ usage.cost }}
</span>
</div>
</div>
<small class="mt-6 block font-bold">
{{ $t('records_updated_daily_bases') }}
</small>
</div>
</template>
<script>
import FormLabel from '../UI/Labels/FormLabel'
import { mapGetters } from 'vuex'
export default {
name: 'UserUsageEstimates',
components: {
FormLabel,
},
computed: {
...mapGetters(['user']),
},
}
</script>
@@ -1,75 +0,0 @@
<template>
<div @click="togglePermission" class="permission-toggle">
<b class="privilege">{{ $t(teamPermissions[permission]) }}</b>
<refresh-cw-icon size="14" />
</div>
</template>
<script>
import { RefreshCwIcon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'PermissionToggleButton',
props: ['item'],
computed: {
...mapGetters(['teamPermissions']),
},
components: {
RefreshCwIcon,
},
data() {
return {
permission: undefined,
}
},
methods: {
togglePermission() {
let index = Object.keys(this.teamPermissions)
.map((i) => i)
.indexOf(this.permission)
if (index === Object.keys(this.teamPermissions).length - 1) {
this.permission = Object.keys(this.teamPermissions)[0]
} else {
this.permission = Object.keys(this.teamPermissions)[index + 1]
}
this.$emit('input', this.permission)
},
},
created() {
this.permission = this.item.permission
},
}
</script>
<style lang="scss" scoped>
@import '../../../../sass/vuefilemanager/inapp-forms';
@import '../../../../sass/vuefilemanager/forms';
.permission-toggle {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
.privilege {
white-space: nowrap;
@include font-size(13);
color: $text-muted;
margin-right: 10px;
}
polyline,
path {
color: $light_text;
}
}
.dark {
.permission-toggle .privilege {
color: $dark_mode_text_secondary;
}
}
</style>
@@ -1,31 +0,0 @@
<template>
<div class="py-3 px-5 text-left">
<div class="info">
<b class="title text-sm">
{{ teamFolder.data.attributes.name }}
</b>
<span class="subtitle mb-2 block text-tiny text-gray-600 dark:text-gray-500">
{{ $t('created_at') }} {{ teamFolder.data.attributes.created_at }}
</span>
<TeamMembersPreview :folder="teamFolder" :avatar-size="32" class="members" />
</div>
</div>
</template>
<script>
import TeamMembersPreview from './TeamMembersPreview'
import { mapGetters } from 'vuex'
export default {
name: 'TeamFolderPreview',
components: {
TeamMembersPreview,
},
computed: {
...mapGetters(['currentTeamFolder', 'clipboard']),
teamFolder() {
return this.currentTeamFolder ? this.currentTeamFolder : this.clipboard[0]
},
},
}
</script>
@@ -1,95 +0,0 @@
<template>
<ul>
<li
v-if="Object.values(members).length > 0 && entry.id !== user.data.id"
v-for="(entry, i) in members"
:key="i"
class="flex items-center py-2"
>
<!--Remove Member-->
<div @click="deleteMember(entry)" class="-ml-1.5 cursor-pointer py-2 px-1 leading-none">
<x-icon size="14" class="vue-feather dark:text-gray-600" />
</div>
<!--Member Preview-->
<div class="flex items-center">
<!--Avatar-->
<MemberAvatar class="mr-3 ml-2" :is-border="false" :size="44" :member="$mapIntoMemberResource(entry)" />
<!--Member-->
<div v-if="entry.type === 'member'" class="info">
<b
class="max-w-1 block overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold"
style="max-width: 155px"
>
{{ entry.name }}
</b>
<span class="block text-xs text-gray-600 dark:text-gray-500">
{{ entry.email }}
</span>
</div>
<!--Invitation-->
<div v-if="entry.type === 'invitation'" class="info">
<b
class="block max-w-xs overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold"
style="max-width: 155px"
>
{{ entry.email }}
</b>
<span v-if="entry.id" class="block text-xs text-gray-600 dark:text-gray-500">
{{ $t('waiting_for_accept_invitation') }}
</span>
</div>
</div>
<!--Set member permission-->
<div class="ml-auto">
<PermissionToggleButton @input="updateMemberPermission(entry, $event)" :item="entry" />
</div>
</li>
</ul>
</template>
<script>
import PermissionToggleButton from './PermissionToggleButton'
import MemberAvatar from '../../UI/Others/MemberAvatar'
import { XIcon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'TeamList',
props: ['value'],
computed: {
...mapGetters(['user']),
},
components: {
PermissionToggleButton,
MemberAvatar,
XIcon,
},
data() {
return {
members: undefined,
}
},
methods: {
updateMemberPermission(member, value) {
this.members.map((e) => (e === member ? (e.permission = value) : e))
this.emitMembers()
},
deleteMember(member) {
this.members = this.members.filter((m) => m !== member)
this.emitMembers()
},
emitMembers() {
this.$emit('input', this.members)
},
},
created() {
this.members = this.value
},
}
</script>
@@ -1,35 +0,0 @@
<template>
<div class="w-28">
<div v-if="!teamFolder" class="text-right md:text-center">
<span class="mr-3 align-middle text-tiny text-gray-600 dark:text-gray-500 md:mr-0.5">
{{ $t('not_selected') }}
</span>
</div>
<TeamMembersPreview
v-else
:folder="teamFolder"
:limit="true"
:avatar-size="size"
class="justify-end md:justify-center"
/>
</div>
</template>
<script>
import TeamMembersPreview from './TeamMembersPreview'
import { mapGetters } from 'vuex'
export default {
name: 'TeamMembersButton',
components: {
TeamMembersPreview,
},
props: ['size'],
computed: {
...mapGetters(['currentTeamFolder', 'clipboard']),
teamFolder() {
return this.currentTeamFolder ? this.currentTeamFolder : this.clipboard[0]
},
},
}
</script>
@@ -1,98 +0,0 @@
<template>
<div class="team-folder">
<span v-if="limit && membersCount > 3" class="member-count"> +{{ membersCount - 3 }} </span>
<div class="members">
<div
v-for="member in members"
:key="member.data.id"
:title="member.data.attributes.email"
class="member-preview z-10"
>
<MemberAvatar :is-border="true" :size="34" :member="member" />
</div>
</div>
</div>
</template>
<script>
import MemberAvatar from '../../UI/Others/MemberAvatar'
export default {
name: 'TeamMembersPreview',
props: ['folder', 'limit', 'avatarSize'],
components: {
MemberAvatar,
},
computed: {
membersCount() {
return (
this.folder.data.relationships.members.data.length +
this.folder.data.relationships.invitations.data.length
)
},
members() {
let allMembers = this.folder.data.relationships.members.data.concat(
this.folder.data.relationships.invitations.data
)
if (this.limit) {
return allMembers.slice(0, 3)
}
return allMembers
},
},
}
</script>
<style lang="scss" scoped>
@import 'resources/sass/vuefilemanager/_variables';
@import 'resources/sass/vuefilemanager/_mixins';
.team-folder {
display: flex;
align-items: center;
.member-count {
@include font-size(12);
color: $text-muted;
margin-right: 3px;
opacity: 0.7;
min-width: 14px;
text-align: left;
}
.members {
display: flex;
.member-preview {
margin-left: -10px;
&:first-child {
margin-left: 0;
}
}
.member {
width: 32px;
height: 32px;
object-fit: cover;
border-radius: 10px;
border: 2px solid white;
vertical-align: middle;
}
}
}
.dark {
.team-folder {
.member-count {
color: $dark_mode_text_secondary;
}
.members .member {
border-color: $dark_mode_foreground;
}
}
}
</style>
@@ -1,260 +0,0 @@
<template>
<PopupWrapper name="create-team-folder">
<!--Title-->
<PopupHeader :title="popupTitle" icon="user-plus" />
<!--Content-->
<PopupContent>
<!--Item Thumbnail-->
<ThumbnailItem v-if="!isNewFolderTeamCreation" class="mb-5" :item="item" />
<!--Form to set team folder-->
<ValidationObserver @submit.prevent="createTeamFolder" ref="teamFolderForm" v-slot="{ invalid }" tag="form">
<!--Set folder name-->
<ValidationProvider
v-if="isNewFolderTeamCreation"
tag="div"
mode="passive"
name="Name"
rules="required"
v-slot="{ errors }"
>
<AppInputText :title="$t('popup_create_folder.label')" :error="errors[0]">
<input
v-model="name"
:class="{ '!border-rose-600': errors[0] }"
type="text"
ref="name"
class="focus-border-theme input-dark"
:placeholder="$t('popup_create_folder.placeholder')"
/>
</AppInputText>
</ValidationProvider>
<!--Add Member-->
<ValidationProvider tag="div" mode="passive" name="Email" v-slot="{ errors }">
<AppInputText :title="$t('add_member')" :error="errors[0]">
<div class="relative">
<span
v-if="email"
@click="addMember"
class="button-base theme absolute right-2 top-1/2 -translate-y-1/2 transform cursor-pointer rounded-lg px-3 py-2 text-sm font-bold"
>
{{ $t('add') }}
</span>
<input
@keypress.enter.stop.prevent="addMember"
ref="email"
v-model="email"
:class="{ '!border-rose-600': errors[0] }"
type="email"
class="focus-border-theme input-dark"
:placeholder="$t('type_member_email_')"
/>
</div>
</AppInputText>
</ValidationProvider>
<!--Member list-->
<ValidationProvider tag="div" mode="passive" name="Members" rules="required" v-slot="{ errors }">
<AppInputText :title="$t('your_members')" :error="errors[0]" :is-last="true">
<span v-if="errors[0]" class="error-message" style="margin-top: -5px">
{{ $t('add_at_least_one_member') }}
</span>
<TeamList v-model="invitations" />
<p v-if="Object.values(invitations).length === 0" class="text-xs dark:text-gray-500">
{{ $t('add_at_least_one_member_into_team_folder') }}
</p>
</AppInputText>
</ValidationProvider>
<InfoBox v-if="!isNewFolderTeamCreation" class="mt-2.5 !mb-0">
<p v-html="$t('popup.move_into_team_disclaimer')"></p>
</InfoBox>
</ValidationObserver>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary"
>{{ $t('cancel') }}
</ButtonBase>
<ButtonBase
class="w-full"
@click.native="createTeamFolder"
button-style="theme"
:loading="isLoading"
:disabled="isLoading"
>{{ popupSubmit }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import AppInputText from '../Forms/Layouts/AppInputText'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import PopupWrapper from '../Popups/Components/PopupWrapper'
import PopupActions from '../Popups/Components/PopupActions'
import PopupContent from '../Popups/Components/PopupContent'
import PopupHeader from '../Popups/Components/PopupHeader'
import ThumbnailItem from '../UI/Entries/ThumbnailItem'
import ButtonBase from '../UI/Buttons/ButtonBase'
import TeamList from './Components/TeamList'
import { required } from 'vee-validate/dist/rules'
import InfoBox from '../UI/Others/InfoBox'
import { events } from '../../bus'
import axios from 'axios'
import { mapGetters } from 'vuex'
export default {
name: 'CreateTeamFolderPopup',
components: {
ValidationProvider,
ValidationObserver,
AppInputText,
TeamList,
ThumbnailItem,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
required,
InfoBox,
},
computed: {
...mapGetters(['user']),
popupTitle() {
return this.item ? this.$t('convert_as_team_folder') : this.$t('create_team_folder')
},
popupSubmit() {
return this.item ? this.$t('move_and_invite_members') : this.$t('create_team_folder')
},
isNewFolderTeamCreation() {
return !this.item
},
},
data() {
return {
invitations: [],
item: undefined,
name: undefined,
email: undefined,
isLoading: false,
}
},
methods: {
async createTeamFolder() {
const isValid = await this.$refs.teamFolderForm.validate()
if (!isValid) return
this.isLoading = true
let route = this.name ? `/api/teams/folders` : `/api/teams/folders/${this.item.data.id}/convert`
let payload = this.name
? {
name: this.name,
invitations: this.invitations,
}
: {
invitations: this.invitations,
}
axios
.post(route, payload)
.then((response) => {
let isTeamFoldersLocation = this.$isThisRoute(this.$route, ['TeamFolders'])
// Redirect into newly created team folder
if (isTeamFoldersLocation && this.$route.params.id) {
this.$router.push({
name: 'TeamFolders',
params: { id: response.data.data.id },
})
// Add created team folder into Team Folder homepage view
} else if (isTeamFoldersLocation && !this.$route.params.id) {
this.$store.commit('ADD_NEW_FOLDER', response.data)
// Redirect to Team Folders after converting simple folder
} else if (!isTeamFoldersLocation) {
this.$router.push({ name: 'TeamFolders' })
}
let toasterMessage = this.isNewFolderTeamCreation
? this.$t('team_was_invited')
: this.$t('team_was_invited_and_folder_moved')
events.$emit('toaster', {
type: 'success',
message: toasterMessage,
})
this.$store.dispatch('getAppData')
})
.catch(() => this.$isSomethingWrong())
.finally(() => {
this.isLoading = false
this.name = undefined
this.invitations = undefined
this.$closePopup()
})
},
addMember() {
if (!this.$isValidEmail(this.email)) {
this.$refs.teamFolderForm.setErrors({
Email: this.$t('type_valid_email'),
})
return
}
if (this.$cantInviteMember(this.email, this.invitations)) {
this.$refs.teamFolderForm.setErrors({
Email: this.$t('upgrade_to_invite_members'),
})
return
}
this.$refs.teamFolderForm.reset()
this.invitations.unshift({
type: 'invitation',
email: this.email,
permission: 'can-edit',
})
this.email = undefined
},
},
created() {
events.$on('popup:open', (args) => {
if (args.name !== 'create-team-folder') return
this.item = args.item
this.$nextTick(() => {
if (this.$isMobile()) return
if (this.item) this.$refs.email.focus()
if (!this.item && this.$refs.name) this.$refs.name.focus()
})
})
events.$on('popup:close', () => {
setTimeout(() => {
this.email = undefined
this.name = undefined
this.item = undefined
this.invitations = []
}, 150)
})
},
}
</script>
@@ -1,239 +0,0 @@
<template>
<PopupWrapper name="update-team-folder">
<!--Title-->
<PopupHeader :title="$t('edit_team_folder')" icon="user-plus" />
<!--Content-->
<PopupContent>
<!--Item Thumbnail-->
<ThumbnailItem class="mb-5" :item="item" />
<!--Form to set team folder-->
<ValidationObserver @submit.prevent="updateTeamFolder" ref="teamFolderForm" v-slot="{ invalid }" tag="form">
<!--Add Member-->
<ValidationProvider tag="div" mode="passive" name="Email" v-slot="{ errors }">
<AppInputText :title="$t('add_member')" :error="errors[0]">
<div class="relative">
<span
v-if="email"
@click="addMember"
class="button-base theme absolute right-2 top-1/2 -translate-y-1/2 transform cursor-pointer rounded-lg px-3 py-2 text-sm font-bold"
>
{{ $t('add') }}
</span>
<!--TODO: Fix !pr-20 after JIT official release-->
<input
@keypress.enter.stop.prevent="addMember"
ref="email"
v-model="email"
:class="{ '!border-rose-600': errors[0] }"
type="email"
class="focus-border-theme input-dark !pr-20"
:placeholder="$t('type_member_email_')"
/>
</div>
</AppInputText>
</ValidationProvider>
<!--Member list-->
<ValidationProvider tag="div" mode="passive" name="Members" v-slot="{ errors }">
<label class="input-label">{{ $t('your_members') }}:</label>
<span v-if="errors[0]" class="error-message" style="margin-top: -5px">{{
$t('add_at_least_one_member')
}}</span>
<TeamList v-model="members" />
<TeamList v-model="invitations" />
<p
v-if="Object.values(members).length === 0 && Object.values(invitations).length === 0"
class="text-xs dark:text-gray-500"
>
{{ $t('add_at_least_one_member_into_team_folder') }}
</p>
</ValidationProvider>
</ValidationObserver>
</PopupContent>
<!--Actions-->
<PopupActions>
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary"
>{{ $t('cancel') }}
</ButtonBase>
<ButtonBase
class="w-full"
@click.native="updateTeamFolder"
:button-style="isDisabledSubmit ? 'secondary' : 'theme'"
:loading="isLoading"
:disabled="isLoading || isDisabledSubmit"
>{{ $t('update_team_folder') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import AppInputText from '../Forms/Layouts/AppInputText'
import { ValidationProvider, ValidationObserver } from 'vee-validate/dist/vee-validate.full'
import PopupWrapper from '../Popups/Components/PopupWrapper'
import PopupActions from '../Popups/Components/PopupActions'
import PopupContent from '../Popups/Components/PopupContent'
import PopupHeader from '../Popups/Components/PopupHeader'
import ThumbnailItem from '../UI/Entries/ThumbnailItem'
import ButtonBase from '../UI/Buttons/ButtonBase'
import TeamList from './Components/TeamList'
import { required } from 'vee-validate/dist/rules'
import InfoBox from '../UI/Others/InfoBox'
import { events } from '../../bus'
import axios from 'axios'
import { mapGetters } from 'vuex'
export default {
name: 'EditTeamFolderPopup',
components: {
ValidationProvider,
ValidationObserver,
AppInputText,
TeamList,
ThumbnailItem,
PopupWrapper,
PopupActions,
PopupContent,
PopupHeader,
ButtonBase,
required,
InfoBox,
},
computed: {
...mapGetters(['user']),
isDisabledSubmit() {
return Object.values(this.members).length === 0 && Object.values(this.invitations).length === 0
},
},
data() {
return {
invitations: [],
members: [],
item: undefined,
name: undefined,
email: undefined,
isLoading: false,
}
},
methods: {
async updateTeamFolder() {
const isValid = await this.$refs.teamFolderForm.validate()
if (!isValid) return
this.isLoading = true
axios
.patch(`/api/teams/folders/${this.item.data.id}`, {
members: this.members,
invitations: this.invitations,
})
.then((response) => {
this.$store.commit('UPDATE_ITEM', response.data)
this.$store.commit('SET_CURRENT_TEAM_FOLDER', response.data)
events.$emit('toaster', {
type: 'success',
message: this.$t('team_folder_updated'),
})
})
.catch(() => {
events.$emit('toaster', {
type: 'danger',
message: this.$t('popup_error.title'),
})
})
.finally(() => {
this.isLoading = false
this.name = undefined
this.invitations = undefined
this.members = undefined
this.$closePopup()
})
},
addMember() {
if (!this.$isValidEmail(this.email)) {
this.$refs.teamFolderForm.setErrors({
Email: this.$t('type_valid_email'),
})
return
}
if (this.$cantInviteMember(this.email, this.invitations)) {
this.$refs.teamFolderForm.setErrors({
Email: this.$t('upgrade_to_invite_members'),
})
return
}
this.$refs.teamFolderForm.reset()
this.invitations.unshift({
type: 'invitation',
email: this.email,
permission: 'can-edit',
})
this.email = undefined
},
},
mounted() {
events.$on('popup:open', (args) => {
if (args.name !== 'update-team-folder') return
this.item = args.item
this.members = args.item.data.relationships.members.data.map((member) => {
return {
type: 'member',
id: member.data.id,
email: member.data.attributes.email,
name: member.data.attributes.name,
avatar: member.data.attributes.avatar,
color: member.data.attributes.color,
permission: member.data.attributes.permission,
}
})
this.invitations = args.item.data.relationships.invitations.data.map((member) => {
return {
id: member.data.id,
type: 'invitation',
email: member.data.attributes.email,
color: member.data.attributes.color,
permission: member.data.attributes.permission,
}
})
this.$nextTick(() => {
if (this.$refs.email && !this.$isMobile()) this.$refs.email.focus()
})
})
events.$on('popup:close', () => {
setTimeout(() => {
this.email = undefined
this.name = undefined
this.item = undefined
this.invitations = []
this.members = []
}, 150)
})
},
}
</script>
<style scoped lang="scss">
@import '../../../sass/vuefilemanager/inapp-forms';
@import '../../../sass/vuefilemanager/forms';
.item-thumbnail {
margin-bottom: 20px;
}
</style>
@@ -1,17 +1,8 @@
<template>
<div
v-if="toasters.length || notifications.length"
v-if="toasters.length"
class="fixed bottom-4 right-4 left-4 z-[55] sm:w-[360px] sm:left-auto lg:bottom-8 lg:right-8"
>
<ToasterWrapper
v-for="notification in notifications"
:key="notification.data.id"
class="mt-4 overflow-hidden rounded-xl dark:bg-2x-dark-foreground bg-white/80 backdrop-blur-2xl shadow-xl"
bar-color="bg-theme"
>
<Notification :notification="notification" class="z-10 !mb-0 !px-4 !pt-4 !pb-5" />
</ToasterWrapper>
<ToasterWrapper
v-for="(toaster, i) in toasters"
:key="i"
@@ -24,7 +15,6 @@
</template>
<script>
import Notification from '../Notifications/Components/Notification'
import ToasterWrapper from './ToasterWrapper'
import {events} from '../../bus'
import Toaster from './Toaster'
@@ -32,12 +22,10 @@ import Toaster from './Toaster'
export default {
components: {
ToasterWrapper,
Notification,
Toaster,
},
data() {
return {
notifications: [],
toasters: [],
}
},
@@ -51,29 +39,6 @@ export default {
},
created() {
events.$on('toaster', (toaster) => this.toasters.push(toaster))
events.$on('notification', (notification) => this.notifications.push(notification))
/*events.$emit('notification', {
data: {
type: 'file-request',
id: 'df954d23-f9d4-4677-85c8-abfd48aaa090',
attributes: {
action: {
type: 'route',
params: {
route: 'Files',
button: 'Show Files',
id: 'ae37b1d8-c147-489a-83ab-2a3c7cb86263',
},
},
created_at: '',
description: "Your file request for 'Multi Level Folder' folder was filled successfully.",
read_at: '',
title: 'File Request Filled',
category: 'file-request',
},
},
})*/
},
}
</script>
@@ -1,41 +0,0 @@
<template>
<div
v-if="config.allowedFacebookLogin || config.allowedGoogleLogin || config.allowedGithubLogin"
class="mb-10 flex items-center justify-center"
>
<div v-if="config.allowedFacebookLogin" class="mx-5 cursor-pointer">
<facebook-icon @click="socialiteRedirect('facebook')" />
</div>
<div v-if="config.allowedGithubLogin" class="mx-5 cursor-pointer">
<github-icon @click="socialiteRedirect('github')" />
</div>
<div v-if="config.allowedGoogleLogin" class="mx-5 cursor-pointer">
<google-icon @click.native="socialiteRedirect('google')" class="vue-feather"/>
</div>
</div>
</template>
<script>
import { FacebookIcon, GithubIcon } from 'vue-feather-icons'
import GoogleIcon from "../../Icons/GoogleIcon"
import { mapGetters } from 'vuex'
export default {
name: 'SocialLoginButtons',
components: {
FacebookIcon,
GoogleIcon,
GithubIcon,
},
computed: {
...mapGetters(['config']),
},
methods: {
socialiteRedirect(provider) {
this.$store.dispatch('socialiteRedirect', provider)
},
},
}
</script>
@@ -11,29 +11,20 @@
</span>
</div>
</div>
<NotificationBell @click.native="openNotificationPopup" />
</div>
</template>
<script>
import MemberAvatar from './MemberAvatar'
import NotificationBell from '../../Notifications/Components/NotificationBell'
import { events } from '../../../bus'
import { mapGetters } from 'vuex'
export default {
name: 'UserHeadline',
components: {
NotificationBell,
MemberAvatar,
},
computed: {
...mapGetters(['user']),
},
methods: {
openNotificationPopup() {
events.$emit('popup:open', { name: 'notifications-mobile' })
},
},
}
</script>
@@ -1,210 +0,0 @@
<template>
<PopupWrapper name="create-file-request">
<!--Title-->
<PopupHeader :title="$t('create_file_request')" icon="upload" />
<!--Content-->
<PopupContent>
<!--Item Thumbnail-->
<ThumbnailItem v-if="pickedItem" class="mb-5" :item="pickedItem" />
<!--Form to set upload request-->
<ValidationObserver
v-if="!generatedUploadRequest"
@submit.prevent="createUploadRequest"
ref="createForm"
v-slot="{ invalid }"
tag="form"
>
<!--Set name-->
<ValidationProvider
tag="div"
mode="passive"
name="Name"
v-slot="{ errors }"
>
<AppInputText :title="$t('folder_name_optional')" :description="$t('folder_name_optional_description')" :error="errors[0]">
<input
v-model="form.name"
:class="{ '!border-rose-600': errors[0] }"
type="text"
ref="input"
class="focus-border-theme input-dark"
:placeholder="$t('type_name_')"
/>
</AppInputText>
</ValidationProvider>
<!--Set note-->
<ValidationProvider tag="div" mode="passive" name="Note" v-slot="{ errors }">
<AppInputText :title="$t('message_optional')" :description="$t('message_optional_description')" :error="errors[0]">
<textarea
v-model="form.notes"
rows="2"
:class="{ '!border-rose-600': errors[0] }"
type="text"
ref="input"
class="focus-border-theme input-dark"
:placeholder="$t('message_for_recipient')"
></textarea>
</AppInputText>
</ValidationProvider>
<!--Send Request by Email-->
<AppInputSwitch
:title="$t('send_request_by_email')"
:description="$t('send_request_by_email_description')"
:is-last="! shareViaEmail"
>
<SwitchInput v-model="shareViaEmail" :state="shareViaEmail" />
</AppInputSwitch>
<!--Set email-->
<ValidationProvider
v-if="shareViaEmail"
tag="div"
mode="passive"
name="Email"
rules="required"
v-slot="{ errors }"
>
<AppInputText :error="errors[0]" class="-mt-2" :is-last="true">
<input
v-model="form.email"
:class="{ '!border-rose-600': errors[0] }"
type="text"
ref="input"
class="focus-border-theme input-dark"
:placeholder="$t('type_email_')"
/>
</AppInputText>
</ValidationProvider>
</ValidationObserver>
<!--Copy generated link-->
<AppInputText v-if="generatedUploadRequest" :title="$t('copy_upload_request_link')" :is-last="true">
<CopyInput :str="generatedUploadRequest.data.attributes.url" />
</AppInputText>
</PopupContent>
<!--Actions-->
<PopupActions v-if="!generatedUploadRequest">
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="secondary"
>{{ $t('cancel') }}
</ButtonBase>
<ButtonBase class="w-full" @click.native="createUploadRequest" :loading="isLoading" button-style="theme"
>{{ $t('create_request') }}
</ButtonBase>
</PopupActions>
<!--Actions-->
<PopupActions v-if="generatedUploadRequest">
<ButtonBase class="w-full" @click.native="$closePopup()" button-style="theme"
>{{ $t('awesome_iam_done') }}
</ButtonBase>
</PopupActions>
</PopupWrapper>
</template>
<script>
import {ValidationProvider, ValidationObserver} from 'vee-validate/dist/vee-validate.full'
import AppInputSwitch from '../Forms/Layouts/AppInputSwitch'
import {required} from 'vee-validate/dist/rules'
import ButtonBase from '../UI/Buttons/ButtonBase'
import AppInputText from '../Forms/Layouts/AppInputText'
import PopupWrapper from '../Popups/Components/PopupWrapper'
import PopupActions from '../Popups/Components/PopupActions'
import PopupContent from '../Popups/Components/PopupContent'
import PopupHeader from '../Popups/Components/PopupHeader'
import SwitchInput from '../Inputs/SwitchInput'
import ThumbnailItem from '../UI/Entries/ThumbnailItem'
import CopyInput from '../Inputs/CopyInput'
import {events} from '../../bus'
import axios from 'axios'
export default {
name: 'CreateUploadRequestPopup',
components: {
ValidationProvider,
ValidationObserver,
AppInputSwitch,
ThumbnailItem,
AppInputText,
PopupWrapper,
PopupActions,
PopupContent,
SwitchInput,
PopupHeader,
ButtonBase,
CopyInput,
required,
},
data() {
return {
form: {
email: undefined,
notes: undefined,
folder_id: undefined,
name: undefined,
},
generatedUploadRequest: undefined,
shareViaEmail: false,
pickedItem: undefined,
isLoading: false,
}
},
methods: {
async createUploadRequest() {
// Validate fields
const isValid = await this.$refs.createForm.validate()
if (!isValid) return
this.isLoading = true
// Send request to get share link
axios
.post(`/api/upload-request`, this.form)
.then((response) => {
this.generatedUploadRequest = response.data
})
.catch(() => {
events.$emit('alert:open', {
title: this.$t('popup_error.title'),
message: this.$t('popup_error.message'),
})
})
.finally(() => {
this.isLoading = false
})
},
},
created() {
events.$on('popup:open', (args) => {
if (args.name === 'create-file-request') {
this.pickedItem = args.item
this.form.folder_id = args.item?.data.id
}
})
// Close popup
events.$on('popup:close', () => {
// Restore data
setTimeout(() => {
this.generatedUploadRequest = undefined
this.pickedItem = undefined
this.shareViaEmail = false
this.form = {
name: undefined,
email: undefined,
notes: undefined,
folder_id: undefined,
}
}, 150)
})
},
}
</script>