Initial commit

This commit is contained in:
Julien Nahum
2022-09-20 21:59:52 +02:00
commit f8e6cd4dd6
479 changed files with 77078 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
<template>
<transition enter-active-class="linear duration-500 overflow-hidden"
enter-class="max-h-0 opacity-0"
enter-to-class="max-h-screen opacity-100"
leave-active-class="linear duration-500 overflow-hidden"
leave-class="max-h-screen opacity-100"
leave-to-class="max-h-0 opacity-0"
>
<div :class="alertClasses" class="border shadow-sm p-2 flex items-center rounded-md">
<div class="flex-grow">
<p class="mb-0 py-2 px-4" :class="textClasses" v-html="message"/>
</div>
<div class="justify-end">
<v-button v-if="type == 'error'" color="red" shade="light" @click="close">
Close
</v-button>
<v-button v-if="type == 'success'" color="green" shade="light" @click="close">
OK
</v-button>
<v-button v-if="type == 'warning'" color="yellow" shade="light" @click="close">
OK
</v-button>
<v-button v-if="type == 'confirmation'" class="mr-1 mb-1" @click="confirm">
Yes
</v-button>
<v-button v-if="type == 'confirmation'" color="gray" shade="light" @click="cancel">
No, cancel
</v-button>
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'Alert',
props: ['type', 'message', 'autoClose', 'confirmationProceed', 'confirmationCancel'],
data () {
return {
timeout: null
}
},
computed: {
alertClasses () {
if (this.type === 'error') return 'bg-red-100 border-red-500'
if (this.type === 'success') return 'bg-green-100 border-green-500'
if (this.type === 'warning') return 'bg-yellow-100 border-yellow-500'
return 'bg-blue-50 border-nt-blue-light'
},
textClasses () {
if (this.type === 'error') return 'text-red-600'
if (this.type === 'success') return 'text-green-600'
if (this.type === 'warning') return 'text-yellow-600'
return 'text-nt-blue'
}
},
mounted () {
if (this.autoClose) {
this.timeout = setTimeout(() => {
this.close()
}, this.autoClose)
}
},
methods: {
/**
* Close the modal.
*/
close () {
clearTimeout(this.timeout)
this.$emit('close')
},
/**
* Confirm and close the modal.
*/
confirm () {
this.confirmationProceed()
this.close()
},
/**
* Cancel and close the modal.
*/
cancel () {
if (this.confirmationCancel) {
this.confirmationCancel()
}
this.close()
}
}
}
</script>

View File

@@ -0,0 +1,41 @@
<template>
<div class="breadcrumbs flex">
<div v-for="(item,index) in path" :key="item.label" class="flex items-center">
<router-link v-if="item.route" class="p-1 hover:bg-blue-50 rounded-md" :to="item.route">
{{ item.label }}
</router-link>
<div v-else class="p-1" :class="{'font-semibold': index===path.length-1}">
{{ item.label }}
</div>
<div v-if="index!==path.length-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Breadcrumb',
props: {
/**
* route: Route object
* label: Label
*/
path: { type: Array }
},
data () {
return {}
},
computed: {},
mounted () {
},
methods: {}
}
</script>

View File

@@ -0,0 +1,100 @@
<template>
<button :type="nativeType" :disabled="loading" :class="`py-${sizes['p-y']} px-${sizes['p-x']}
bg-${color}-${colorShades['main']} hover:bg-${color}-${colorShades['hover']} focus:ring-${color}-${colorShades['ring']}
focus:ring-offset-${color}-${colorShades['ring-offset']} text-${colorShades['text']}
transition ease-in duration-200 text-center text-${sizes['font']} font-semibold shadow-md focus:outline-none focus:ring-2
focus:ring-offset-2 rounded-lg`"
class="btn" @click="$emit('click',$event)"
>
<template v-if="!loading">
<slot />
</template>
<loader v-else class="h-6 w-6 text-white mx-auto" />
</button>
</template>
<script>
export default {
name: 'VButton',
props: {
color: {
type: String,
default: 'nt-blue'
},
shade: {
type: String,
default: 'normal'
},
size: {
type: String,
default: 'medium'
},
nativeType: {
type: String,
default: 'submit'
},
loading: {
type: Boolean,
default: false
}
},
computed: {
colorShades () {
if (this.color === 'nt-blue') {
return {
main: 'default',
hover: 'light',
ring: 'light',
'ring-offset': 'lighter',
text: 'white'
}
}
if (this.shade === 'lighter') {
return {
main: '200',
hover: '300',
ring: '100',
'ring-offset': '50',
text: 'gray-900'
}
}
if (this.shade === 'light') {
return {
main: '400',
hover: '500',
ring: '300',
'ring-offset': '150',
text: 'white'
}
}
return {
main: '600',
hover: '700',
ring: '500',
'ring-offset': '200',
text: 'white'
}
},
sizes () {
if (this.size === 'small') {
return {
font: 'sm',
'p-y': '1',
'p-x': '2'
}
}
return {
font: 'base',
'p-y': '2',
'p-x': '4'
}
}
}
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="flex flex-col w-full bg-white rounded-lg shadow"
:class="{'px-4 py-8 sm:px-6 md:px-8 lg:px-10':padding}"
>
<div v-if="title" class="self-center mb-6 text-xl font-light text-gray-900 sm:text-3xl font-bold dark:text-white">
{{ title }}
</div>
<slot />
</div>
</template>
<script>
export default {
name: 'Card',
props: {
padding: {
type: Boolean,
default: true
},
title: {
type: String,
default: null
}
}
}
</script>

View File

@@ -0,0 +1,40 @@
<template>
<div>
<div class="w-full relative">
<div class="cursor-pointer" @click="trigger">
<slot name="title" />
</div>
<div class="text-gray-400 hover:text-gray-600 absolute -right-2 -top-1 cursor-pointer p-2" @click="trigger">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 transition transform duration-500" :class="{'rotate-180':showContent}" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v3.586L7.707 9.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 10.586V7z" clip-rule="evenodd" />
</svg>
</div>
</div>
<v-transition>
<div v-if="showContent" class="w-full">
<slot />
</div>
</v-transition>
</div>
</template>
<script>
import VTransition from './transitions/VTransition'
export default {
name: 'Collapse',
components: { VTransition },
props: {
defaultValue: { type: Boolean, default: false }
},
data () {
return {
showContent: this.defaultValue
}
},
methods: {
trigger () {
this.showContent = !this.showContent
}
}
}
</script>

View File

@@ -0,0 +1,53 @@
<template>
<div class="relative">
<div>
<slot name="trigger"
:toggle="toggle"
:open="open"
:close="close"
/>
</div>
<transition name="fade">
<div
v-if="isOpen"
v-on-clickaway="close"
:class="dropdownClass"
>
<div class="py-1 " role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
<slot />
</div>
</div>
</transition>
</div>
</template>
<script>
import { directive as onClickaway } from 'vue-clickaway'
export default {
name: 'Dropdown',
directives: {
onClickaway: onClickaway
},
props: {
dropdownClass: { type: String, default: 'origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-50' }
},
data () {
return {
isOpen: false
}
},
methods: {
open () {
this.isOpen = true
},
close () {
this.isOpen = false
},
toggle () {
this.isOpen = !this.isOpen
}
}
}
</script>

View File

@@ -0,0 +1,57 @@
<template>
<div ref="parent"
tabindex="0"
:class="{
'hover:bg-gray-100 dark:hover:bg-gray-800 rounded px-2 cursor-pointer': !editing
}"
class="relative"
:style="{height: editing?(divHeight+'px'):'auto'}"
@focus="startEditing"
>
<slot v-if="!editing" :content="content">
<label class="cursor-pointer truncate w-full">
{{ content }}
</label>
</slot>
<div v-if="editing" class="absolute inset-0 border-2 transition-colors"
:class="{'border-transparent':!editing,'border-blue-500':editing}">
<input ref="editinput" v-model="content"
class="absolute inset-0 focus:outline-none bg-white transition-colors"
:class="[{'bg-blue-50':editing},contentClass]" @blur="editing = false" @keyup.enter="editing = false"
@input="handleInput"
>
</div>
</div>
</template>
<script>
export default {
props: {
value: {required: true},
textAlign: {type: String, default: 'left'},
contentClass: {type: String | Object, default: ''}
},
data() {
return {
content: this.value,
editing: false,
divHeight: 0
}
},
methods: {
startEditing() {
this.divHeight = this.$refs.parent.offsetHeight
this.editing = true
this.$nextTick(() => {
this.$refs.editinput.focus()
})
},
handleInput(e) {
this.$emit('input', this.content)
}
}
}
</script>

View File

@@ -0,0 +1,103 @@
<template>
<router-link :class="`py-${sizes['p-y']} px-${sizes['p-x']}
bg-${color}-${colorShades['main']} hover:bg-${color}-${colorShades['hover']} focus:ring-${color}-${colorShades['ring']}
focus:ring-offset-${color}-${colorShades['ring-offset']} text-${colorShades['text']}
transition ease-in duration-200 text-center text-${sizes['font']} font-semibold shadow-md focus:outline-none focus:ring-2
focus:ring-offset-2 rounded-lg hover:no-underline inline-block`" :to="to" :target="target"
>
<template v-if="!loading">
<slot />
</template>
<loader v-else class="h-6 w-6 text-white mx-auto" />
</router-link>
</template>
<script>
export default {
name: 'FancyLink',
props: {
to: {
type: Object
},
color: {
type: String,
default: 'nt-blue'
},
target: {
type: String,
default: '_self'
},
shade: {
type: String,
default: 'normal'
},
size: {
type: String,
default: 'medium'
},
loading: {
type: Boolean,
default: false
}
},
computed: {
colorShades () {
if (this.color === 'nt-blue') {
return {
main: 'default',
hover: 'light',
ring: 'light',
'ring-offset': 'lighter',
text: 'white'
}
}
if (this.shade === 'lighter') {
return {
main: '200',
hover: '300',
ring: '100',
'ring-offset': '50',
text: 'gray-900'
}
}
if (this.shade === 'light') {
return {
main: '400',
hover: '500',
ring: '300',
'ring-offset': '150',
text: 'white'
}
}
return {
main: '600',
hover: '700',
ring: '500',
'ring-offset': '200',
text: 'white'
}
},
sizes () {
if (this.size === 'small') {
return {
font: 'sm',
'p-y': '1',
'p-x': '2'
}
}
return {
font: 'base',
'p-y': '2',
'p-x': '4'
}
}
}
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<svg class="animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</template>
<script>
export default {
name: 'Loader',
props: {}
}
</script>

View File

@@ -0,0 +1,79 @@
<template>
<div class="inline" v-if="shouldDisplayProTag">
<div class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold cursor-pointer"
@click.prevent="showPremiumModal=true"
>
PRO
</div>
<modal :show="showPremiumModal" @close="showPremiumModal=false">
<h2 class="text-nt-blue">
OpenForm PRO
</h2>
<h4 v-if="user.is_subscribed && !user.has_enterprise_subscription" class="text-center mt-5">
We're happy to have you as a Pro customer. If you're having any issue with OpenForm, or if you have a
feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
<br><br>
If you need to collaborate, or to work with multiple workspaces, or just larger file uploads, you can
also upgrade our subscription to get an Enterprise subscription.
</h4>
<h4 v-if="user.is_subscribed && user.has_enterprise_subscription" class="text-center mt-5">
We're happy to have you as an Enterprise customer. If you're having any issue with OpenForm, or if you have a
feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
</h4>
<p v-if="!user.is_subscribed" class="mt-4">
All the features with a<span
class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold mx-1"
>
PRO
</span> tag are available in the Pro plan of OpenForm. <b>You can play around and try all Pro features
within
the form editor, but you can't use them in your real forms</b>. You can subscribe now to gain unlimited access
to
all our pro features!
</p>
<p class="my-4 text-center">
Feel free to <a href="mailto:contact@opnform.com">contact us</a> if you have any feature request.
</p>
<div class="mb-4 text-center">
<v-button color="gray" shade="light" @click="showPremiumModal=false">
Close
</v-button>
</div>
</modal>
</div>
</template>
<script>
import Modal from '../Modal'
import axios from 'axios'
import { mapGetters } from 'vuex'
export default {
name: 'ProTag',
components: { Modal },
props: {},
data () {
return {
showPremiumModal: false,
checkoutLoading: false
}
},
computed: {
...mapGetters({
user: 'auth/user',
currentWorkSpace: 'open/workspaces/getCurrent',
}),
shouldDisplayProTag() {
return false; //!this.user.is_subscribed && !(this.currentWorkSpace.is_pro || this.currentWorkSpace.is_enterprise);
},
},
mounted () {
},
methods: {}
}
</script>

View File

@@ -0,0 +1,184 @@
<template>
<div class="scroll-shadow max-w-full" :class="[$style.wrap,{'w-max':!shadow.left && !shadow.right}]">
<div
ref="scrollContainer"
:class="[$style['scroll-container'],{'no-scrollbar':hideScrollbar}]"
:style="{ width: width?width:'auto', height }"
@scroll.passive="toggleShadow"
>
<slot />
<span :class="[$style['shadow-top'], shadow.top && $style['is-active']]" :style="{
top: shadowTopOffset+'px',
}"
/>
<span :class="[$style['shadow-right'], shadow.right && $style['is-active']]" />
<span :class="[$style['shadow-bottom'], shadow.bottom && $style['is-active']]" />
<span :class="[$style['shadow-left'], shadow.left && $style['is-active']]" />
</div>
</div>
</template>
<script>
function newResizeObserver (callback) {
// Skip this feature for browsers which
// do not support ResizeObserver.
// https://caniuse.com/#search=resizeobserver
if (typeof ResizeObserver === 'undefined') return
return new ResizeObserver(e => e.map(callback))
}
export default {
name: 'ScrollShadow',
props: {
hideScrollbar: {
type: Boolean,
default: false
},
shadowTopOffset: {
type: Number,
default: 0
}
},
data () {
return {
width: undefined,
height: undefined,
shadow: {
top: false,
right: false,
bottom: false,
left: false
},
debounceTimeout: null
}
},
mounted () {
window.addEventListener('resize', this.calcDimensions)
// Check if shadows are necessary after the element is resized.
const scrollContainerObserver = newResizeObserver(this.toggleShadow)
if (scrollContainerObserver) {
scrollContainerObserver.observe(this.$refs.scrollContainer)
// Cleanup when the component is destroyed.
this.$once('hook:destroyed', () => scrollContainerObserver.disconnect())
}
// Recalculate the container dimensions when the wrapper is resized.
const wrapObserver = newResizeObserver(this.calcDimensions)
if (wrapObserver) {
wrapObserver.observe(this.$el)
// Cleanup when the component is destroyed.
this.$once('hook:destroyed', () => wrapObserver.disconnect())
}
},
destroyed () {
window.removeEventListener('resize', this.calcDimensions)
},
methods: {
async calcDimensions () {
// Reset dimensions for correctly recalculating parent dimensions.
this.width = undefined
this.height = undefined
await this.$nextTick()
this.width = `${this.$el.clientWidth}px`
this.height = `${this.$el.clientHeight}px`
},
// Check if shadows are needed.
toggleShadow () {
const hasHorizontalScrollbar =
this.$refs.scrollContainer.clientWidth <
this.$refs.scrollContainer.scrollWidth
const hasVerticalScrollbar =
this.$refs.scrollContainer.clientHeight <
this.$refs.scrollContainer.scrollHeight
const scrolledFromLeft =
this.$refs.scrollContainer.offsetWidth +
this.$refs.scrollContainer.scrollLeft
const scrolledFromTop =
this.$refs.scrollContainer.offsetHeight +
this.$refs.scrollContainer.scrollTop
const scrolledToTop = this.$refs.scrollContainer.scrollTop === 0
const scrolledToRight =
scrolledFromLeft >= this.$refs.scrollContainer.scrollWidth
const scrolledToBottom =
scrolledFromTop >= this.$refs.scrollContainer.scrollHeight
const scrolledToLeft = this.$refs.scrollContainer.scrollLeft === 0
this.$nextTick(() => {
this.shadow.top = hasVerticalScrollbar && !scrolledToTop
this.shadow.right = hasHorizontalScrollbar && !scrolledToRight
this.shadow.bottom = hasVerticalScrollbar && !scrolledToBottom
this.shadow.left = hasHorizontalScrollbar && !scrolledToLeft
})
}
}
}
</script>
<style lang="scss" module>
.wrap {
overflow: hidden;
position: relative;
}
.scroll-container {
overflow: auto;
}
.shadow-top,
.shadow-right,
.shadow-bottom,
.shadow-left {
position: absolute;
border-radius: 6em;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.shadow-top,
.shadow-bottom {
right: 0;
left: 0;
height: 1em;
border-top-right-radius: 0;
border-top-left-radius: 0;
background-image: linear-gradient(rgba(#555, 0.1) 0%, rgba(#FFF, 0) 100%);
}
.shadow-top {
top: 0;
}
.shadow-bottom {
bottom: 0;
transform: rotate(180deg);
}
.shadow-right,
.shadow-left {
top: 0;
bottom: 0;
width: 1em;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background-image: linear-gradient(90deg, rgba(#555, 0.1) 0%, rgba(#FFF, 0) 100%);
}
.shadow-right {
right: 0;
transform: rotate(180deg);
}
.shadow-left {
left: 0;
}
.is-active {
opacity: 1;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div class="py-4" :class="{'border-b-2':borderBottom}">
<div class="uppercase tracking-wide text-xs font-bold dark:text-gray-400 text-gray-500 mb-1 leading-tight">
Step: {{ Math.min(current + 1, steps.length) }} of {{ steps.length }}
</div>
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
<div class="flex-1">
<div class="text-lg font-bold dark:text-gray-300 text-gray-700 leading-tight">
{{ steps[current] ? steps[current] : 'Complete!' }}
</div>
</div>
<div class="flex items-center md:w-64">
<div class="w-full bg-gray-100 dark:bg-gray-700 rounded-full mr-2">
<div class="rounded-full bg-nt-blue text-xs leading-none h-2 text-center text-white transition-all"
:style="{'width': parseInt(current / steps.length * 100) +'%', 'min-width': '8px'}"
/>
</div>
<div class="text-xs w-10 text-gray-600 dark:text-gray-400" v-text="parseInt(current / steps.length * 100) +'%'" />
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Steps',
props: {
steps: {
type: Array,
required: true
},
borderBottom: {
type: Boolean,
default: true
},
current: {
type: Number,
default: 0
}
}
}
</script>

15
resources/js/components/common/index.js vendored Normal file
View File

@@ -0,0 +1,15 @@
import Vue from 'vue'
import Dropdown from './Dropdown'
import Card from './Card'
import Button from './Button'
import FancyLink from './FancyLink';
// Components that are registered globaly.
[
FancyLink,
Card,
Button,
Dropdown
].forEach(Component => {
Vue.component(Component.name, Component)
})

View File

@@ -0,0 +1,19 @@
<template>
<transition v-if="name=='slideInUp'"
enter-active-class="linear duration-300 overflow-hidden"
enter-class="max-h-0"
enter-to-class="max-h-screen"
leave-active-class="linear duration-300 overflow-hidden"
leave-class="max-h-screen"
leave-to-class="max-h-0"
>
<slot />
</transition>
</template>
<script>
export default {
name: 'VTransition',
props: { name: { default: 'slideInUp' } }
}
</script>