Skip to content

Commit

Permalink
Merge pull request #25 from Dashibase/fix-router
Browse files Browse the repository at this point in the history
Fix router, add sort and filter, multiple select and dark mode
  • Loading branch information
greentfrapp committed May 18, 2022
2 parents 354b7be + 3cd18ed commit 0741d95
Show file tree
Hide file tree
Showing 40 changed files with 1,681 additions and 749 deletions.
323 changes: 171 additions & 152 deletions package-lock.json

Large diffs are not rendered by default.

24 changes: 13 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,29 @@
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@headlessui/vue": "^1.5.0",
"@headlessui/vue": "^1.6.1",
"@heroicons/vue": "^1.0.6",
"@supabase/supabase-js": "^1.34.1",
"@tailwindcss/forms": "^0.5.0",
"@supabase/supabase-js": "^1.35.3",
"@tailwindcss/forms": "^0.5.2",
"lodash": "^4.17.21",
"pinia": "^2.0.14",
"pinia-plugin-persistedstate": "^1.5.2",
"vue": "^3.2.25",
"vue-router": "^4.0.14"
"vue-router": "^4.0.15"
},
"devDependencies": {
"@types/lodash": "^4.14.182",
"@types/node": "^17.0.25",
"@vitejs/plugin-vue": "^2.2.0",
"autoprefixer": "^10.4.4",
"postcss": "^8.4.12",
"@types/node": "^17.0.34",
"@vitejs/plugin-vue": "^2.3.3",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.13",
"tailwindcss": "^3.0.24",
"typescript": "^4.5.4",
"vite": "^2.8.0",
"typescript": "^4.6.4",
"vite": "^2.9.9",
"vue-tsc": "^0.29.8"
}
}
17 changes: 0 additions & 17 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,3 @@
<template>
<router-view />
</template>

<script setup lang="ts">
import config from './dashibaseConfig'
import { store } from './utils/store'
import { Page } from './utils/config'
import { isHostedByDashibase } from './utils/supabase'
if (isHostedByDashibase) {
store.appName = window.localStorage.getItem('dashibase.app_name') as string || 'Dashibase'
store.pages = JSON.parse(window.localStorage.getItem('dashibase.pages') as string) as Page[] || []
} else {
store.appName = config.name || 'Dashibase'
store.pages = config.pages || []
}
document.title = store.appName
</script>
37 changes: 35 additions & 2 deletions src/components/Main.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
<template>
<Dashboard />
<router-view />
<div class="z-50 absolute w-full top-0 bg-white transition-all duration-500"
:class="store.dashboard.name ? 'opacity-0 pointer-events-none' : 'opacity-100'">
<Placeholder />
</div>
</template>

<script setup lang="ts">
import Dashboard from './dashboard/Dashboard.vue'
import { useRoute } from 'vue-router'
import { User as SupabaseUser } from '@supabase/supabase-js'
import { supabase } from '@/utils/supabase'
import { useStore } from '@/utils/store'
import Placeholder from './dashboard/Placeholder.vue'
const store = useStore()
const route = useRoute()
if (!store.user.id && !['/signin', '/signup'].includes(route.path)) window.location.href = '/signin'
const intervalId = setInterval(() => {
if (supabase) {
clearInterval(intervalId)
store.dashboard.supabaseAnonKey = window.localStorage.getItem('dashibase.supabase_anon_key') || ''
store.dashboard.supabaseUrl = window.localStorage.getItem('dashibase.supabase_url') || ''
store.dashboard.name = window.localStorage.getItem('dashibase.app_name') || ''
store.dashboard.id = window.localStorage.getItem('dashibase.dashboard_id') || ''
const user = supabase.auth.user()
if (user) store.user = user
supabase.auth.onAuthStateChange((_, session) => {
store.user = session?.user as SupabaseUser
})
if (!store.user.id) {
if (!['/signin', '/signup'].includes(route.path)) window.location.href = '/signin'
}
}
}, 100)
</script>
7 changes: 5 additions & 2 deletions src/components/branding/AppLogo.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<template>
<h2 class="text-lg text-center font-bold text-gray-700 select-none">{{ store.appName }}</h2>
<h2 class="text-lg text-center font-bold select-none transition" :class="store.darkMode ? 'text-neutral-300' : 'text-neutral-700'">
{{ store.dashboard.name }}
</h2>
</template>

<script setup lang="ts">
import { store } from '../../utils/store'
import { useStore } from '@/utils/store'
const store = useStore()
</script>
10 changes: 8 additions & 2 deletions src/components/branding/PoweredBy.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<template>
<a class="text-gray-400 font-medium text-sm" href="https://dashibase.com">
<a class="font-medium text-sm" :class="store.darkMode ? 'text-neutral-500' : 'text-neutral-400'" href="https://dashibase.com">
Powered by Dashibase
</a>
</template>
</template>

<script setup lang="ts">
import { useStore } from '@/utils/store'
const store = useStore()
</script>
104 changes: 65 additions & 39 deletions src/components/dashboard/CardView.vue
Original file line number Diff line number Diff line change
@@ -1,66 +1,92 @@
<template>
<div class="w-full">
<div class="px-4 md:px-10 py-12 flex items-center">
<h1 class="text-2xl font-medium">{{ page.name }}</h1>
<div class="px-4 md:px-10 py-12 flex flex-col sm:flex-row justify-between gap-4 sm:items-end">
<PageHeader>{{ page.name }}</PageHeader>
<div class="flex gap-2">
<FilterMenu :attributes="page.attributes" @close="filterItems"/>
<DeleteButton v-if="selected.length" @click="deleteCards">
Delete
</DeleteButton>
<PrimaryButton v-if="!selected.length" @click="createCard">
New
</PrimaryButton>
</div>
</div>
<!-- Warning -->
<div v-if="warning" class="py-2 px-4 md:px-10 text-sm text-red-500">
{{ warning }}
</div>
<div class="px-4 md:px-10 grid grid-cols-1 md:grid-cols-2 gap-2">
<div class="px-4 md:px-10 grid grid-cols-1 md:grid-cols-2 gap-2" :class="store.darkMode ? 'text-neutral-200' : 'text-neutral-800'">
<!-- Cards -->
<a v-for="(item, i) in items" :key="item.id" class="bg-white block relative hover:bg-gray-50 cursor-pointer border rounded px-2 py-1" :href="`/${page.page_id}/view/${item.id}`">
<div class="absolute top-0 left-0 w-full text-sm text-gray-300 text-right px-1">
#{{ i + 1 + ((paginationNum - 1) * maxItems) }}
</div>
<div class="flex flex-col gap-2 p-2">
<div v-for="attribute in page.attributes" :key="attribute.id"
class="font-medium text-gray-900 flex flex-col gap-1">
<div class="text-gray-300 text-sm">{{ attribute.label }}</div>
<div class="truncate">{{ item[attribute.id] }}</div>
<button v-for="(item, i) in items" :key="item.id" class="text-left block relative cursor-pointer border rounded px-2 py-1"
:class="store.darkMode ? 'border-neutral-700' : 'border-neutral-300'"
@click.exact="router.push(`/${page.page_id}/view/${item.id}`)"
@click.shift.left.exact="event => selectCard(i, event)">
<div class="flex flex-col gap-1 p-2">
<div class="font-medium text-lg flex items-center justify-between">
<div class="truncate">{{ item[page.attributes[0].id] }}</div>
<input v-if="selected.length" type="checkbox" class="cursor-pointer focus:outline-none focus:ring-0 focus:ring-offset-0 focus:border-0 h-4 w-4 rounded text-neutral-700"
:class="store.darkMode ? 'bg-neutral-900 border-neutral-600' : 'border-neutral-300'" :checked="selected.includes(i)"
@click="event => selectCard(i, event)" />
</div>
<div v-for="attribute in page.attributes.slice(1)" :key="attribute.id"
class="flex flex-col">
<div class="text-xs" :class="store.darkMode ? 'text-neutral-700' : 'text-neutral-300'">{{ attribute.label }}</div>
<div class="-mt-0.5 truncate text-sm">{{ item[attribute.id] }}</div>
</div>
</div>
<div v-if="!page.readonly" class="absolute bottom-0 right-0 text-sm text-gray-300 hover:text-red-600 flex justify-end px-1 py-1" @click="event => deleteItem(item.id, event)">
<TrashIcon class="w-4 h-4 cursor-pointer" />
</div>
</a>
<!-- New Item -->
<a v-if="!page.readonly" class="w-full border rounded px-4 py-3 text-gray-300 cursor-pointer hover:border-green-200 hover:bg-green-100 hover:text-green-400 flex justify-between"
:href="`/${page.page_id}/new`">
<div class="font-medium">New item</div>
<PlusIcon class="h-5" />
</a>
</button>
</div>
<Pagination class="mt-10 px-10" :paginationList="paginationList" :maxPagination="maxPagination" v-model="paginationNum" />
<Pagination v-if="maxPagination > 1" class="mt-10 px-10" :paginationList="paginationList" :maxPagination="maxPagination" v-model="paginationNum" />
<DeleteModal ref="deleteModal" />
</div>
</template>

<script setup lang="ts">
import { PropType } from 'vue'
import {
PlusIcon,
TrashIcon,
} from '@heroicons/vue/solid'
import { Page } from '../../utils/config'
import { initLoading, initCrud } from '../../utils/dashboard'
import DropDown from './form-elements/DropDown.vue'
import { ref, PropType } from 'vue'
import router from '@/router'
import { Page } from '@/utils/config'
import { useStore } from '@/utils/store'
import { initCrud } from '@/utils/dashboard'
import Pagination from './Pagination.vue'
import PageHeader from './elements/PageHeader.vue'
import PrimaryButton from './elements/PrimaryButton.vue'
import FilterMenu from './elements/FilterMenu.vue'
import DeleteButton from './elements/DeleteButton.vue'
import DeleteModal from './DeleteModal.vue'
const store = useStore()
const selected = ref([] as number[])
const props = defineProps({
loading: {
type: Boolean,
default: false,
},
page: {
type: Object as PropType<Page>,
required: true,
},
})
const deleteModal = ref<any|null>(null)
const { items, page, warning, paginationNum, maxPagination, paginationList, deleteItems, filterItems } = initCrud(props.page)
const maxItems = 10
function createCard () {
router.push(`/${props.page.page_id}/new`)
}
const { loading } = initLoading(props.loading)
const { page, warning, items, paginationNum, maxPagination, paginationList, getItems, deleteItem } = initCrud(loading, props.page, maxItems)
function selectCard (idx:number, event:Event) {
event.stopPropagation()
if (!selected.value.includes(idx)) selected.value.push(idx)
else selected.value.splice(selected.value.indexOf(idx), 1)
}
getItems()
async function deleteCards () {
if (!deleteModal.value) return
deleteModal.value.title = 'Confirm deletion'
deleteModal.value.message = `Are you sure you want to delete ${selected.value.length > 1 ? 'these rows' : 'this row'}?`
const confirm = await deleteModal.value?.confirm()
if (confirm) {
deleteItems(selected.value.map((idx:number) => items.value[idx].id))
.then(() => selected.value = [])
}
}
</script>
92 changes: 58 additions & 34 deletions src/components/dashboard/CreateItem.vue
Original file line number Diff line number Diff line change
@@ -1,65 +1,89 @@
<template>
<div class="space-y-6 sm:px-6 lg:px-0 w-full">
<div class="bg-white py-6 px-4 sm:p-6 mx-auto w-full">
<div class="px-10 py-12 flex items-center text-2xl font-medium gap-2">
<h1>{{ page.name }}</h1>
<ChevronRightIcon class="inline text-gray-500 h-6 w-auto" />
<h1>New Item</h1>
<div class="py-6 px-4 sm:p-6 mx-auto w-full">
<div class="px-4 md:px-10 py-12 flex justify-between items-end">
<PageHeader>
<h1 class="cursor-pointer" @click="router.push(`/${page.page_id}`)">{{ page.name }}</h1>
<ChevronRightIcon class="inline text-neutral-500 h-6 w-auto" />
<h1>New Item</h1>
</PageHeader>
</div>
<div class="mt-6 flex flex-col gap-6" v-if="page.attributes">
<!-- Attribute Inputs -->
<div v-for="attribute in page.attributes" :key="attribute.id"
class="px-4 md:px-10">
<label :for="attribute.id" class="block text-sm font-medium text-gray-700">{{ attribute.label }} <span v-if="attribute.required" class="text-gray-400 font-normal pl-2">required</span></label>
<input v-if="attribute.type === AttributeType.Date" type="date" :disabled="loading" :id="attribute.id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm" />
<select v-else-if="attribute.type === AttributeType.Bool" :disabled="loading" :id="attribute.id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm">
<option :value="true">true</option>
<option :value="false">false</option>
</select>
<select v-else-if="attribute.type === AttributeType.Enum" :disabled="loading" :id="attribute.id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm">
class="px-4 md:px-10 transition" :class="store.darkMode ? 'text-neutral-200' : 'text-neutral-800'">
<label :for="attribute.id" class="block text-sm font-medium transition" :class="store.darkMode ? 'text-neutral-400' : 'text-neutral-600'">
{{ attribute.label }}
<span v-if="attribute.required" class="font-normal pl-2 transition" :class="store.darkMode ? 'text-neutral-600' : 'text-neutral-400'">required</span>
</label>
<input v-if="attribute.type === AttributeType.Date" type="date" :disabled="store.loading" :id="attribute.id"
class="mt-1 block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-0 sm:text-sm transition"
:class="store.darkMode ? 'bg-neutral-900 border-neutral-700 focus:border-neutral-500' : 'bg-white border-neutral-300 focus:border-neutral-500'"
@input="update(attribute.id, ($event.target as HTMLInputElement).value)" />
<Toggle v-else-if="attribute.type === AttributeType.Bool" class="mt-1" :modelValue="item[attribute.id] ? item[attribute.id] : false"
@update:modelValue="(value:any) => update(attribute.id, value)" />
<select v-else-if="attribute.type === AttributeType.Enum" :disabled="store.loading" :id="attribute.id"
class="mt-1 block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-0 sm:text-sm cursor-pointer transition"
:class="store.darkMode ? 'bg-neutral-900 border-neutral-700 focus:border-neutral-500' : 'bg-white border-neutral-300 focus:border-neutral-500'"
@input="update(attribute.id, ($event.target as HTMLInputElement).value)">
<option v-for="option in attribute.enumOptions" :key="option" :value="option">{{ option }}</option>
</select>
<textarea v-else-if="attribute.type === AttributeType.LongText" :disabled="loading" :id="attribute.id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm" />
<input v-else type="text" :disabled="loading" :id="attribute.id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm" />
<textarea v-else-if="attribute.type === AttributeType.LongText" :disabled="store.loading" :id="attribute.id"
class="mt-1 block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-0 sm:text-sm transition"
:class="store.darkMode ? 'bg-neutral-900 border-neutral-700 focus:border-neutral-500' : 'bg-white border-neutral-300 focus:border-neutral-500'"
@input="update(attribute.id, ($event.target as HTMLInputElement).value)" />
<input v-else type="text" :disabled="store.loading" :id="attribute.id"
class="mt-1 block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-0 sm:text-sm transition"
:class="store.darkMode ? 'bg-neutral-900 border-neutral-700 focus:border-neutral-500' : 'bg-white border-neutral-300 focus:border-neutral-500'"
@input="update(attribute.id, ($event.target as HTMLInputElement).value)" />
</div>
<!-- Warning -->
<div v-if="warning" class="px-4 md:px-10 text-sm text-red-500">
{{ warning }}
</div>
<!-- Buttons -->
<div class="px-4 md:px-10 flex justify-end gap-4">
<button :disabled="loading" class="bg-white border border-transparent rounded-md py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
@click="router.go(-1)">
<TertiaryButton :disabled="store.loading" @click="router.go(-1)">
Back
</button>
<button :disabled="loading" class="bg-green-500 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
@click="createItem">
{{ loading ? 'Loading...' : 'Save' }}
</button>
</TertiaryButton>
<PrimaryButton :disabled="store.loading" @click="createItem(item)">
Save
</PrimaryButton>
</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { PropType } from 'vue'
import { ref, computed } from 'vue'
import { ChevronRightIcon } from '@heroicons/vue/solid'
import router from '../../router'
import { initLoading, initCrud } from '../../utils/dashboard'
import { Page, AttributeType } from '../../utils/config'
import router from '@/router'
import { useStore } from '@/utils/store'
import { initCrud } from '@/utils/dashboard'
import { Page, AttributeType } from '@/utils/config'
import PageHeader from './elements/PageHeader.vue'
import TertiaryButton from './elements/TertiaryButton.vue'
import PrimaryButton from './elements/PrimaryButton.vue'
const store = useStore()
const props = defineProps({
loading: {
type: Boolean,
default: false,
},
page: {
type: Object as PropType<Page>,
pageId: {
type: String,
required: true,
},
})
const { loading } = initLoading(props.loading)
const { page, warning, createItem } = initCrud(loading, props.page)
const page = computed(():Page => {
return store.dashboard.pages.find(page => page.page_id === props.pageId) || {} as Page
})
const item = ref({} as {[k:string]:any})
function update (attributeId:string, newVal:any) {
item.value[attributeId] = newVal
}
const { warning, createItem } = initCrud(page.value)
</script>

0 comments on commit 0741d95

Please sign in to comment.