package list from viandwi24

This commit is contained in:
Kar
2023-04-28 09:58:09 +05:30
commit 4125135289
108 changed files with 27411 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
<template>
<div
class="bg-gray-100/[0.8] dark:bg-slate-800/[0.8] backdrop-blur supports-backdrop-blur:bg-white/60 p-4 rounded overflow-y-auto"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,14 @@
<script lang="ts" setup>
defineProps({
text: {
type: String,
default: '',
},
})
</script>
<template>
<div class="text-xs font-bold text-center mb-2">
<slot>{{ text }}</slot>
</div>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<div class="fixed bg-black opacity-70 z-50 top-0 left-0 w-screen h-screen" />
</template>

View File

@@ -0,0 +1,64 @@
<script lang="ts" setup>
import { TransitionRoot, TransitionChild } from '@headlessui/vue'
// micro compiler
const emit = defineEmits(['onClose'])
// state
const show = ref(false)
// methods
const close = () => {
show.value = false
setTimeout(() => emit('onClose'), 100)
}
// lifecycle
onMounted(() => {
setTimeout(() => (show.value = true), 100)
})
</script>
<template>
<Teleport to="body">
<TransitionRoot :show="show" appear>
<div>
<ActionSheetOverlay @click="close" />
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-300 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div
class="fixed bottom-0 w-screen z-50 flex"
style="max-height: 66.666667%"
>
<div
class="relative max-w-8xl px-4 pb-4 w-full mx-auto flex flex-col flex-1 space-y-1 overflow-y-auto justify-end"
>
<slot />
</div>
</div>
</TransitionChild>
</div>
</TransitionRoot>
</Teleport>
</template>
<style lang="scss">
.slide-fade-from-bottom-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-from-bottom-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-from-bottom-enter-from,
.slide-fade-from-bottom-leave-to {
transform: translateY(20px);
opacity: 0;
}
</style>

107
components/Alert.vue Normal file
View File

@@ -0,0 +1,107 @@
<script lang="ts" setup>
import { TransitionRoot, TransitionChild } from '@headlessui/vue'
export type IStyles = 'primary' | 'success' | 'warning' | 'danger'
// props
const props = defineProps({
title: {
type: String,
default: undefined,
},
text: {
type: String,
default: undefined,
},
type: {
type: String,
default: 'primary',
},
})
// styles
const styles = reactive<{
[key: string]: string
}>({
primary: '',
success:
'dark:from-green-500/50 via-gray-200 to-gray-200 dark:via-slate-800 dark:to-slate-800',
warning:
'dark:from-yellow-500/50 via-gray-200 to-gray-200 dark:via-slate-800 dark:to-slate-800',
danger:
'dark:from-red-500/50 via-gray-200 to-gray-200 dark:via-slate-800 dark:to-slate-800',
})
const textStyles = reactive<{
[key: string]: string
}>({
primary: 'text-white',
success: 'text-green-500',
warning: 'text-orange-500',
danger: 'text-red-500',
})
// selected
const isDestroyed = ref<Boolean>(false)
const selectedType = computed<IStyles>((): IStyles => {
if (['primary', 'success', 'warning', 'danger'].includes(props.type))
return props.type as IStyles
return 'primary'
})
const selectedStyle = computed(() => styles[selectedType.value])
const selectedTextStyle = computed(() => textStyles[selectedType.value])
// actions
const close = () => {
isDestroyed.value = true
}
</script>
<template>
<TransitionRoot :show="!isDestroyed" appear>
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-300 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div
:class="`bg-gray-200 dark:bg-slate-800 bg-gradient-to-r shadow-white/50 dark:shadow-slate-900/50 px-6 py-6 rounded-md shadow-lg flex space-x-6 ${selectedStyle}`"
>
<div class="flex items-center justify-center">
<slot name="icon">
<IconMdi:checkCircle
v-if="selectedType === 'success'"
:class="`text-2xl ${selectedTextStyle}`"
/>
<icon-clarity:times-circle-solid
v-if="selectedType === 'danger'"
:class="`text-2xl ${selectedTextStyle}`"
/>
<icon-bi:exclamation-circle-fill
v-if="selectedType === 'warning'"
:class="`text-2xl ${selectedTextStyle}`"
/>
</slot>
</div>
<div class="flex-1">
<div :class="`font-bold text-lg mb-0.5 ${selectedTextStyle}`">
<slot name="title">{{ props.title }}</slot>
</div>
<div class="text-gray-700 dark:text-gray-100">
<slot name="title">{{ props.text }}</slot>
</div>
</div>
<div>
<button
class="text-slate-600 hover:text-red-500 dark:text-gray-400 font-bold"
@click="close"
>
<icon-clarity:times-line />
</button>
</div>
</div>
</TransitionChild>
</TransitionRoot>
</template>

39
components/Anchor.vue Normal file
View File

@@ -0,0 +1,39 @@
<script lang="ts" setup>
// micro compiler
const props = defineProps({
text: {
type: String,
default: '',
},
to: {
type: [String, Object],
default: undefined,
},
href: {
type: String,
default: '',
},
})
// state
const href = toRef(props, 'href')
const to = toRef(props, 'to')
</script>
<template>
<NuxtLink
v-if="to"
tag="a"
:to="to"
:class="`transition-colors duration-300 dark:hover:text-white hover:text-gray-900 hover:underline`"
>
<slot>{{ text }}</slot>
</NuxtLink>
<a
v-else
:class="`transition-colors duration-300 dark:hover:text-white hover:text-gray-900 hover:underline`"
:href="href"
>
<slot>{{ text }}</slot>
</a>
</template>

View File

@@ -0,0 +1,171 @@
<script lang="ts" setup>
import { AppConfigInput } from '@nuxt/schema'
// state
const app = useAppConfig() as AppConfigInput
const navbar = ref(null)
const showDrawer = useState<boolean>('navbar.showDrawer', () => false)
const showOptions = useState<boolean>('navbar.showOptions', () => false)
// lifecycle
let timer: NodeJS.Timer
onMounted(() => {
if (!navbar.value) return
// scroll
const { onScroll } = useSticky(navbar.value, 0)
setTimeout(() => onScroll(), 50)
// on show on mobile
setInterval(() => {
// must in mobile
const minW = 1024
if (window.innerWidth < minW) {
updateDrawerOptions()
}
}, 100)
})
onBeforeUnmount(() => {
if (timer) clearInterval(timer)
})
// methods
const updateDrawerOptions = () => {
// drawer
if (showDrawer.value || showOptions.value) {
document.body.classList.add('overflow-hidden')
} else {
document.body.classList.remove('overflow-hidden')
}
}
const toggleDrawer = () => (showDrawer.value = !showDrawer.value)
const toggleOptions = (show?: boolean) => {
if (show) {
showOptions.value = show
} else {
showOptions.value = !showOptions.value
}
}
</script>
<template>
<div
ref="navbar"
class="backdrop-filter backdrop-blur-md top-0 z-40 w-full flex-none transition-colors duration-300 lg:z-50 border-b border-gray-900/10 dark:border-gray-50/[0.2] bg-white/[0.5] dark:bg-slate-900/[0.5]"
>
<div id="navbar-banner" class="banner">
<slot name="banner" />
</div>
<div class="max-w-8xl w-full mx-auto">
<div class="py-3 lg:px-8 mx-4 lg:mx-0">
<div class="relative flex items-center">
<!-- drawer:toggle -->
<div
v-if="$slots['drawer']"
class="lg:hidden flex items-center self-center justify-center mr-2"
>
<button
class="flex items-center focus:outline-none"
aria-label="Toggle Drawer Menu"
@click="toggleDrawer()"
>
<span
class="flex items-center text-gray-600 dark:text-gray-300 text-lg"
aria-hidden="true"
>
<IconUil:bars v-if="!showDrawer" />
<IconUil:times v-else />
</span>
</button>
</div>
<!-- title -->
<slot name="title">
<NuxtLink
tag="a"
class="mr-3 flex-none overflow-hidden md:w-auto text-md font-bold text-gray-900 dark:text-gray-200"
:to="{ name: 'index' }"
>
<span class="sr-only">home</span>
<span class="flex items-center">
<IconSimpleIcons:nuxtdotjs
class="inline-block mr-2 text-lg text-primary-500"
/>
{{ app.name }}
</span>
</NuxtLink>
</slot>
<!-- menu -->
<slot name="menu" />
<!-- options:toggle -->
<div
v-if="$slots['options']"
class="flex-1 flex justify-end lg:hidden"
>
<button
class="flex items-center focus:outline-none"
aria-label="Toggle Options Menu"
@click="toggleOptions()"
>
<span
class="flex items-center text-gray-600 dark:text-gray-300 text-sm"
aria-hidden="true"
>
<icon-fa-solid:ellipsis-v />
</span>
</button>
</div>
</div>
</div>
</div>
<ClientOnly>
<Teleport to="#app-after">
<!-- drawer -->
<Transition name="slide-fade-from-up" mode="out-in">
<div
v-if="showDrawer && $slots['drawer']"
class="fixed lg:hidden bg-gray-100 dark:bg-slate-800 pt-12 top-0 left-0 w-screen h-screen z-30 flex flex-col"
>
<div class="flex-1 flex flex-col relative overflow-y-auto">
<slot name="drawer" :toggle-drawer="toggleDrawer" />
</div>
</div>
</Transition>
<!-- options -->
<div v-if="showOptions && $slots['options']">
<slot
name="options"
:toggle-options="toggleOptions"
:show-options="showOptions"
/>
</div>
</Teleport>
</ClientOnly>
</div>
</template>
<style lang="scss">
.slide-fade-from-up-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-from-up-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-from-up-enter-from,
.slide-fade-from-up-leave-to {
transform: translateY(-20px);
opacity: 0;
}
a.router-link-active {
font-weight: bold;
}
a.router-link-exact-active {
color: theme('colors.slate.900');
}
html.dark {
a.router-link-exact-active {
color: theme('colors.white');
}
}
</style>

86
components/Button.vue Normal file
View File

@@ -0,0 +1,86 @@
<script lang="ts" setup>
const props = defineProps({
text: {
type: String,
default: '',
},
type: {
type: String,
default: 'primary',
},
size: {
type: String,
default: 'md',
},
to: {
type: [String, Object],
default: undefined,
},
href: {
type: String,
default: undefined,
},
})
// state:styles
const defaultStyle = `
cursor-pointer
border transition-color duration-300
focus:outline-none focus:ring-1 focus:ring-offset-1 focus:dark:ring-offset-gray-50 focus:dark:ring-gray-400 focus:ring-gray-600/[0.6] focus:ring-offset-gray-800/[0.6]
flex items-center justify-center font-semibold
`
const styles = reactive<{
[key: string]: string
}>({
none: '',
primary: 'text-white bg-primary-500 hover:bg-primary-400 border-primary-500',
secondary:
'text-slate-800 bg-gray-200 border-gray-200 hover:bg-gray-300 dark:text-white dark:border-slate-800 dark:bg-slate-800 dark:hover:bg-slate-700',
opposite:
'text-white bg-gray-800 hover:bg-white hover:text-gray-800 hover:border-gray-900 dark:text-gray-800 dark:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-gray-100 dark:border-white',
})
const sizes = reactive<{
[key: string]: string
}>({
lg: 'h-13 px-8 text-lg rounded-lg',
md: 'h-10 px-6 text-base rounded',
sm: 'h-9 px-4 text-sm rounded',
xs: 'h-6 px-3 text-xs rounded',
})
// state
const selectedStyle = computed(() =>
props.type in styles ? styles[props.type] : styles.primary
)
const selectedSize = computed(() => sizes[props.size] || sizes.lg)
// methods
const onClick = (event: MouseEvent) => {
const router = useRouter()
if (props.to) {
router.push(props.to)
}
if (!props.href) {
event.preventDefault()
}
}
</script>
<template>
<NuxtLink
v-if="to"
tag="a"
:to="to"
:class="`${defaultStyle} ${selectedStyle} ${selectedSize}`"
>
<slot>{{ text }}</slot>
</NuxtLink>
<a
v-else
:class="`${defaultStyle} ${selectedStyle} ${selectedSize}`"
:href="href"
@click="onClick"
>
<slot>{{ text }}</slot>
</a>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<div class="card-content px-6 py-6 relative">
<slot />
</div>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<div
class="card-footer px-6 py-2 text-sm bg-white dark:bg-slate-800 border-t border-gray-900/10 dark:border-gray-50/[0.2]"
>
<slot />
</div>
</template>

14
components/Card/Title.vue Normal file
View File

@@ -0,0 +1,14 @@
<script lang="ts" setup>
defineProps({
text: {
type: String,
default: '',
},
})
</script>
<template>
<div class="text-xl font-semibold mb-2">
<slot>{{ text }}</slot>
</div>
</template>

20
components/Card/index.vue Normal file
View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
defineProps({
disabled: {
type: Boolean,
required: false,
},
})
</script>
<template>
<div
class="card duration-300 transition-colors w-full relative rounded overflow-hidden bg-white dark:bg-slate-900 border border-gray-900/10 dark:border-gray-50/[0.2]"
>
<div
v-if="disabled"
class="absolute z-10 top-0 left-0 w-full h-full bg-slate-800/[0.75] cursor-not-allowed"
/>
<slot />
</div>
</template>

View File

@@ -0,0 +1,65 @@
<template>
<BuilderNavbar>
<template #menu>
<div class="relative hidden lg:flex items-center ml-auto">
<div class="flex items-center justify-center">
<img
class="w-6 h-6 rounded-full"
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"
alt="Avatar of Jonathan Reinink"
/>
<span class="ml-2 text-sm font-semibold">Alfian</span>
<IconUil:angle-down />
</div>
<div
class="flex space-x-4 border-l ml-6 pl-6 border-gray-900/10 dark:border-gray-50/[0.2]"
>
<LanguageSwitcher />
<ThemeSwitcher />
<Anchor
class="hover:no-underline hover:text-slate-900 hover:dark:text-white text-lg flex self-center items-center"
href="boilarplate"
title="Github"
>
<IconMdi:github-face />
</Anchor>
</div>
</div>
</template>
<template #options="{ toggleOptions }">
<ActionSheet @on-close="toggleOptions(false)">
<ActionSheetBody>
<ActionSheetHeader text="Menu" />
<div class="mt-6 text-sm font-bold capitalize">
{{ $t('components.theme_switcher.change_theme') }}
</div>
<div class="mt-2">
<ThemeSwitcher type="select-box" />
</div>
<div class="mt-6 text-sm font-bold capitalize">
{{ $t('components.language_switcher.change_language') }}
</div>
<div class="mt-2">
<LanguageSwitcher type="select-box" />
</div>
</ActionSheetBody>
<Button
type="secondary"
title="Github"
href="boilarplate"
>
<IconMdi:github-face />
<span class="ml-1">Github</span>
</Button>
<Button
text="Close"
type="secondary"
@click.prevent="toggleOptions(false)"
/>
</ActionSheet>
</template>
<template #drawer>
<slot name="drawer" />
</template>
</BuilderNavbar>
</template>

View File

@@ -0,0 +1,64 @@
<script lang="ts">
export default defineComponent({
props: {
mode: {
type: String,
default: 'normal',
},
},
setup() {
const sidebar = ref(null)
onMounted(() => {
// const { onScroll } = useSticky(sidebar.value, -1000)
// setTimeout(() => onScroll(), 50)
})
return {
sidebar,
}
},
})
</script>
<template>
<div
ref="sidebar"
:class="{
'fixed top-0 hidden pt-12 lg:flex lg:w-60 xl:w-80 h-screen':
mode === 'normal',
'relative flex-1 flex flex-col w-full': mode === 'mobile',
}"
>
<div class="flex-1 overflow-y-auto pl-4 lg:pl-0 pr-4 py-4">
<ul>
<li v-for="i in 29" :key="i">
<Anchor
:to="{ name: 'dashboard' }"
class="group flex items-center mb-4 hover:no-underline"
>
<div
class="flex items-center mr-4 px-2 py-2 rounded-md ring-1 ring-slate-900/5 shadow-sm group-hover:shadow group-hover:ring-slate-900/10 dark:ring-0 dark:shadow-none dark:group-hover:shadow-none dark:group-hover:highlight-white/10 group-hover:shadow-sky-200 dark:highlight-white/10"
:class="{
'text-white dark:text-white group-hover:bg-sky-500 bg-sky-500':
i === 1,
'text-slate-500 dark:text-gray-100 group-hover:bg-gray-200 bg-gray-100 dark:group-hover:bg-slate-600 dark:bg-slate-700':
i !== 1,
}"
>
<IconUil:apps class="text-xs" />
</div>
<span
class="text-sm font-semibold capitalize"
:class="{
'font-extrabold text-sky-500 dark:text-sky-400': i === 1,
}"
>
{{ $t('pages.dashboard.index.nav') }}
</span>
</Anchor>
</li>
</ul>
</div>
</div>
</template>

View File

@@ -0,0 +1,9 @@
<template>
<LayoutPage>
<Error :code="404" wrap />
</LayoutPage>
</template>
<script setup>
import LayoutPage from '~/layouts/page.vue'
</script>

46
components/Error.vue Normal file
View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
// components
const PageWrapper = resolveComponent('PageWrapper')
// compiler macro
const props = defineProps({
code: {
type: Number,
default: 400,
},
wrap: {
type: Boolean,
default: false,
},
})
// computed
const errorsMap: {
[key: string]: string
} = {
'400': 'Bad Request',
'401': 'Unauthorized',
'403': 'Forbidden',
'404': 'Not Found',
}
const error = computed(() => {
const { code } = props
return {
code,
message: errorsMap[code.toString()] || 'Unknown Error',
}
})
</script>
<template>
<component
:is="props.wrap ? PageWrapper : 'div'"
:class="props.wrap ? 'flex flex-col items-center justify-center' : ''"
>
<h1 class="text-center mb-6 leading-3">
<span class="font-bold text-8xl block">{{ error.code }}</span>
<span class="block italic">{{ error.message }}</span>
</h1>
<Button text="Home" to="/" size="sm" />
</component>
</template>

View File

@@ -0,0 +1,76 @@
<script lang="ts" setup>
// compiler macro
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
on: {
type: Boolean,
default: false,
},
id: {
type: String,
default: undefined,
},
})
const emit = defineEmits(['update:modelValue'])
// random
const randomId = () =>
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
// state
const id = ref(props.id || randomId())
const input = ref<HTMLInputElement>()
// funcs
const checked = useSyncProps<boolean>(props, 'modelValue', emit)
const onInputChange = (e: Event) => {
const target = e.target as HTMLInputElement
checked.value = target.checked
emit('update:modelValue', target.checked)
}
// lifecycle
onMounted(() => {
if (props.on) {
checked.value = true
emit('update:modelValue', true)
if (input.value) input.value.checked = true
}
})
</script>
<template>
<label :for="id" class="flex cursor-pointer">
<label
:for="id"
class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in"
>
<input
:id="id"
ref="input"
type="checkbox"
class="switch-checkbox absolute block w-6 h-6 rounded-full bg-white dark:bg-slate-900 border-2 border-slate-300 dark:border-slate-600 appearance-none cursor-pointer"
:checked="checked"
@change="onInputChange"
/>
<label
:for="id"
class="switch-label block overflow-hidden h-6 rounded-full bg-gray-200 dark:bg-slate-700 cursor-pointer border border-slate-300 dark:border-slate-500"
/>
</label>
<slot />
</label>
</template>
<style>
.switch-checkbox:checked {
right: 0;
}
.switch-checkbox:checked + .switch-label {
@apply bg-primary-500;
}
</style>

View File

@@ -0,0 +1,97 @@
<script lang="ts" setup>
// compiler macro
const props = defineProps({
modelValue: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
size: {
type: String,
default: 'md',
},
type: {
type: String,
default: 'default',
},
})
const emit = defineEmits(['update:modelValue'])
const slots = useSlots()
// list styles
const paddingStyles = reactive<{
[key: string]: string
}>({
xs: 'px-1 py-0.5',
sm: 'px-2 py-1.5',
md: 'px-4 py-2',
lg: 'px-5 py-3',
})
const fontSizeStyles = reactive<{
[key: string]: string
}>({
xs: 'text-xs',
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg',
})
// states
const modelValue = useSyncProps<string>(props, 'modelValue', emit)
const havePreEl = computed(
() =>
typeof slots.prefix !== 'undefined' ||
typeof slots['prefix-disabled'] !== 'undefined'
)
const haveSuEl = computed(() => typeof slots.suffix !== 'undefined')
const selectedBorderStyle = computed(
() => 'border-gray-900/10 dark:border-gray-50/[0.2]'
)
const selectedOnHoverBorderStyle = computed(
() => 'dark:focus:border-white focus:border-gray-900'
)
const selectedPaddingStyle = computed(
() => paddingStyles[props.size] || paddingStyles.md
)
const selectedFontSizeStyle = computed(
() => fontSizeStyles[props.size] || fontSizeStyles.md
)
</script>
<template>
<div :class="`text-input-container relative flex`">
<div
v-if="slots['prefix-disabled']"
:class="`flex rounded-l bg-gray-100 dark:bg-slate-800 text-gray-500 border ${selectedBorderStyle}`"
>
<slot name="prefix-disabled" />
</div>
<div
v-if="slots.prefix"
:class="`flex rounded-l border ${selectedBorderStyle}`"
>
<slot name="prefix" />
</div>
<div class="text-input-wrapper relative flex flex-1">
<input
v-model="modelValue"
:class="`text-input w-full flex-1 bg-transparent outline-none border ${
havePreEl ? '' : 'rounded-l'
} ${
haveSuEl ? '' : 'rounded-r'
} ${selectedBorderStyle} ${selectedOnHoverBorderStyle} ${selectedPaddingStyle} ${selectedFontSizeStyle}`"
:type="type"
:placeholder="placeholder"
/>
</div>
<div
v-if="slots.suffix"
:class="`flex rounded-r border ${selectedBorderStyle}`"
>
<slot name="suffix" />
</div>
</div>
</template>

143
components/Gem.vue Normal file
View File

@@ -0,0 +1,143 @@
<script>
// taken from https://github.com/nuxt/framework/blob/main/docs/components/atoms/Gem.vue
export default {
data() {
return {
ready: false,
}
},
async mounted() {
const THREE = await import('three').then((m) => m.default || m)
const { OrbitControls } = await import(
'three/examples/jsm/controls/OrbitControls.js'
).then((m) => m.default || m)
const { GLTFLoader } = await import(
'three/examples/jsm/loaders/GLTFLoader.js'
).then((m) => m.default || m)
// Canvas
let canvas = document.querySelector('canvas.webgl')
// wait
while (!canvas) {
await new Promise((resolve) => setTimeout(resolve, 50))
canvas = document.querySelector('canvas.webgl')
}
// Scene
const scene = new THREE.Scene()
// Models
let gem
let light
const gltfLoader = new GLTFLoader()
gltfLoader.load('/assets/gem/gem.gltf', (gltf) => {
// Gem
gem = gltf.scene.children[6]
// Material setup
// const textureLoader = new THREE.TextureLoader()
// const roughnessTexture = textureLoader.load('/assets/gem/roughness.jpeg')
// gem.material.roughnessMap = roughnessTexture
gem.material.displacementScale = 0.15
gem.material.emissiveIntensity = 0.4
gem.material.refractionRatio = 1
gem.rotation.z = 0
// change color
scene.add(gem)
light = gltf.scene.children[0]
scene.add(light)
this.ready = true
})
// Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 3)
directionalLight.position.set(1, 1, 1)
scene.add(directionalLight)
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 3)
directionalLight2.position.set(-1, -1, -1)
scene.add(directionalLight2)
// Settings
const sizes = {
width: 500,
height: 500,
}
// Base camera
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
0.1,
100
)
camera.position.set(2, 2, 6)
scene.add(camera)
// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableZoom = false
controls.target.set(0, 0.75, 0)
controls.enableDamping = true
controls.enablePan = false
// Lock Y Axis
controls.minPolarAngle = Math.PI / 2
controls.maxPolarAngle = Math.PI / 2
// Render
const renderer = new THREE.WebGLRenderer({
antialiasing: true,
canvas,
alpha: true,
})
renderer.setClearColor(0x000000, 0)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
// Animations
const clock = new THREE.Clock()
let previousTime = 0
const tick = () => {
const elapsedTime = clock.getElapsedTime()
const _deltaTime = elapsedTime - previousTime
previousTime = elapsedTime
if (gem) {
gem.rotation.y = 1.1 * elapsedTime
}
// Update controls
controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
tick()
},
}
</script>
<template>
<canvas class="webgl" :style="{ opacity: ready ? 1 : 0 }" />
</template>
<style scoped>
.webgl {
outline: none;
width: 300px;
height: 300px;
opacity: 0;
transition: opacity 1s ease;
}
</style>

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOptions,
ListboxOption,
} from '@headlessui/vue'
import { availableLocales } from '~/utils/lang'
// micro compiler
const props = defineProps({
type: {
type: String,
default: 'dropdown-right-top',
},
})
// state
const currentStyle = toRef(props, 'type')
const localeSetting = useState<string>('locale.setting')
</script>
<template>
<div class="flex items-center">
<Listbox
v-if="currentStyle === 'dropdown-right-top'"
v-model="localeSetting"
as="div"
class="relative flex items-center"
>
<ListboxLabel class="sr-only">Theme</ListboxLabel>
<ListboxButton
type="button"
title="Change Language"
class="transition-colors duration-300"
>
<span class="justify-center items-center flex">
<IconLa:language />
</span>
</ListboxButton>
<ListboxOptions
class="p-1 absolute z-50 top-full right-0 outline-none bg-white rounded-lg ring-1 ring-gray-900/10 shadow-lg overflow-hidden w-36 py-1 text-sm text-gray-700 font-semibold dark:bg-gray-800 dark:ring-0 dark:highlight-white/5 dark:text-gray-300"
>
<ListboxOption
v-for="lang in availableLocales"
:key="lang.iso"
:value="lang.iso"
:class="{
'py-2 px-2 flex items-center cursor-pointer': true,
'text-sky-500 bg-gray-100 dark:bg-gray-600/30':
localeSetting === lang.iso,
'hover:bg-gray-50 dark:hover:bg-gray-700/30':
localeSetting !== lang.iso,
}"
>
<span class="text-sm mr-2">
{{ lang.flag }}
</span>
<span class="flex-1 truncate">
{{ lang.name }}
<span class="text-xs">({{ lang.iso }})</span>
</span>
</ListboxOption>
</ListboxOptions>
</Listbox>
<select
v-if="currentStyle === 'select-box'"
v-model="localeSetting"
class="w-full px-2 pr-3 py-1 outline-none rounded border bg-transparent text-gray-700 dark:text-gray-300 border-gray-900/10 dark:border-gray-50/[0.2]"
>
<option
v-for="lang in availableLocales"
:key="lang.iso"
:value="lang.iso"
class="flex items-center space-x-2"
>
{{ lang.flag }} {{ lang.name }} ({{ lang.iso }})
</option>
</select>
</div>
</template>

5
components/Page/Body.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<div>
<slot />
</div>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
const props = defineProps({
emptyTip: {
type: String,
required: false,
default: 'This page is empty',
},
})
</script>
<template>
<ContentDoc>
<template #default="{ doc }">
<PageHeader>
<PageTitle :text="doc.title" />
</PageHeader>
<PageBody>
<PageSection>
<ContentRenderer :value="doc" />
</PageSection>
</PageBody>
</template>
<template #empty>
<h1>{{ emptyTip }}</h1>
</template>
<template #not-found>
<Error :code="404" wrap />
</template>
</ContentDoc>
</template>

View File

@@ -0,0 +1,29 @@
<script setup>
const props = defineProps({
path: {
type: String,
required: true,
},
pageTitle: {
type: String,
default: '',
},
})
const { data } = await useAsyncData(props.path, () =>
queryContent(props.path).findOne()
)
</script>
<template>
<PageWrapper>
<PageHeader>
<PageTitle :text="pageTitle" class="capitalize" />
</PageHeader>
<PageBody>
<PageSection>
<ContentRenderer :value="data" />
</PageSection>
</PageBody>
</PageWrapper>
</template>

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
import { AppConfigInput } from '@nuxt/schema'
import p from './../../package.json'
const app = useAppConfig() as AppConfigInput
</script>
<template>
<footer class="border-t lg:border-gray-900/10 dark:border-gray-50/[0.2]">
<section
class="max-w-8xl mx-auto px-4 lg:px-8 flex-1 flex w-full space-x-20"
>
<div class="w-full py-4 text-center md:text-left">
<div class="mb-1">
{{ app.name }}
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
Copyright © 2022 <a :href="app.author.link">{{ app.author.name }}</a
>. All rights reserved. Made with <span class="text-red-500"></span>
<div
class="flex flex-col md:flex-row space-x-2 items-center md:float-right"
>
<span class="text-center md:text-right">
design by <a href="#link">dwd</a>
</span>
<span
class="block bg-blue-500 rounded px-1 py-0.5 text-white text-xs"
>
{{ p.devDependencies.nuxt }}
</span>
</div>
</div>
</div>
</section>
</footer>
</template>

View File

@@ -0,0 +1,11 @@
<script lang="ts">
export default defineComponent({
layout: 'dashboard',
})
</script>
<template>
<div class="lg:px-8 px-4 mb-6">
<slot />
</div>
</template>

150
components/Page/Navbar.vue Normal file
View File

@@ -0,0 +1,150 @@
<script lang="ts" setup>
import { AppConfigInput } from '@nuxt/schema'
export interface IMenuItem {
type: 'link' | 'button'
text: string
href?: any
route?: any
}
const { t } = useLang()
const app = useAppConfig() as AppConfigInput
const menus = computed((): IMenuItem[] => [
{
type: 'link',
text: t('pages.getting-started.nav'),
route: { name: 'getting-started' },
},
{ type: 'link', text: t('pages.blank.nav'), route: { name: 'blank' } },
{ type: 'link', text: t('pages.test.nav'), route: { name: 'test' } },
{ type: 'link', text: t('pages.post.nav'), route: { name: 'post' } },
{ type: 'link', text: t('pages.setting.nav'), route: { name: 'setting' } },
{
type: 'button',
text: t('pages.dashboard.nav'),
route: { name: 'dashboard' },
},
])
</script>
<template>
<BuilderNavbar>
<template #banner>
<div
class="text-white text-xs text-center py-1 px-4 lg:px-8 bg-primary-500 capitalize"
>
<span class="mr-1">
{{ $t('banners.welcome', { app_name: app.name }) }}
<Anchor
class="underline font-bold"
:text="$t('others.learn_more')"
href="boilarplate"
/>
</span>
</div>
</template>
<template #menu>
<div class="relative hidden lg:flex items-center ml-auto">
<nav
class="text-sm leading-6 font-semibold text-gray-600 dark:text-gray-300"
role="navigation"
>
<ul class="flex items-center space-x-8">
<li v-for="(item, i) in menus" :key="i">
<Anchor
v-if="item.type === 'link'"
:to="item.route ? item.route : undefined"
:href="item.href ? item.href : undefined"
class="hover:no-underline hover:text-slate-900 hover:dark:text-white capitalize"
>{{ item.text }}</Anchor
>
<Button
v-else-if="item.type === 'button'"
:text="item.text"
size="xs"
class="font-extrabold capitalize"
:to="item.route ? item.route : undefined"
:href="item.href ? item.href : undefined"
/>
</li>
</ul>
</nav>
<div
class="flex space-x-4 border-l ml-6 pl-6 border-gray-900/10 dark:border-gray-50/[0.2]"
>
<LanguageSwitcher />
<ThemeSwitcher />
<Anchor
class="hover:no-underline hover:text-slate-900 hover:dark:text-white text-lg flex self-center items-center"
href="boilarplate"
title="Github"
>
<IconMdi:github-face />
</Anchor>
</div>
</div>
</template>
<template #options="{ toggleOptions }">
<ActionSheet @on-close="toggleOptions(false)">
<ActionSheetBody>
<ActionSheetHeader text="Menu" />
<nav class="leading-6 font-semibold text-gray-600 dark:text-gray-300">
<ul class="flex flex-col">
<li
v-for="(item, i) in menus"
:key="i"
class="flex w-full"
:class="{
'pb-2 mb-2 border-b border-gray-900/10 dark:border-gray-50/[0.2]':
item.type === 'link',
}"
>
<Anchor
v-if="item.type === 'link'"
:to="item.route ? item.route : undefined"
:href="item.href ? item.href : undefined"
class="flex-1 hover:no-underline capitalize"
>{{ item.text }}</Anchor
>
<Button
v-else-if="item.type === 'button'"
:text="item.text"
size="xs"
class="flex-1 font-extrabold capitalize"
:to="item.route ? item.route : undefined"
:href="item.href ? item.href : undefined"
/>
</li>
</ul>
</nav>
<div class="mt-6 text-sm font-bold capitalize">
{{ $t('components.theme_switcher.change_theme') }}
</div>
<div class="mt-2">
<ThemeSwitcher type="select-box" />
</div>
<div class="mt-6 text-sm font-bold capitalize">
{{ $t('components.language_switcher.change_language') }}
</div>
<div class="mt-2">
<LanguageSwitcher type="select-box" />
</div>
</ActionSheetBody>
<Button
type="secondary"
title="Github"
href="boilarplate"
>
<IconMdi:github-face />
<span class="ml-1">Github</span>
</Button>
<Button
text="Close"
type="secondary"
@click.prevent="toggleOptions(false)"
/>
</ActionSheet>
</template>
</BuilderNavbar>
</template>

View File

@@ -0,0 +1,14 @@
<script lang="ts" setup>
defineProps({
text: {
type: String,
default: '',
},
})
</script>
<template>
<div class="text-2xl font-semibold mb-2">
<slot>{{ text }}</slot>
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<section class="lg:px-8 px-4 mb-6">
<slot />
</section>
</template>

14
components/Page/Title.vue Normal file
View File

@@ -0,0 +1,14 @@
<script lang="ts" setup>
defineProps({
text: {
type: String,
default: '',
},
})
</script>
<template>
<div class="text-4xl font-bold">
<slot>{{ text }}</slot>
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<div class="flex-1 relative py-8">
<slot />
</div>
</template>

View File

@@ -0,0 +1,86 @@
<script lang="ts" setup>
import {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOptions,
ListboxOption,
} from '@headlessui/vue'
import { IThemeSettingOptions, availableThemes } from '~/utils/theme'
// micro compiler
const props = defineProps({
type: {
type: String,
default: 'dropdown-right-top',
},
})
// state
const themeSetting = useState<IThemeSettingOptions>('theme.setting')
const currentStyle = toRef(props, 'type')
</script>
<template>
<div class="flex items-center">
<Listbox
v-if="currentStyle === 'dropdown-right-top'"
v-model="themeSetting"
as="div"
class="relative flex items-center"
>
<ListboxLabel class="sr-only">
{{ $t('components.theme_switcher.theme') }}
</ListboxLabel>
<ListboxButton
type="button"
:title="$t('components.theme_switcher.change_theme')"
class="transition-colors duration-300"
>
<span class="flex justify-center items-center dark:hidden">
<IconUil:sun />
</span>
<span class="justify-center items-center hidden dark:flex">
<IconUil:moon />
</span>
</ListboxButton>
<ListboxOptions
class="p-1 absolute z-50 top-full right-0 outline-none bg-white rounded-lg ring-1 ring-gray-900/10 shadow-lg overflow-hidden w-36 py-1 text-sm text-gray-700 font-semibold dark:bg-gray-800 dark:ring-0 dark:highlight-white/5 dark:text-gray-300"
>
<ListboxOption
v-for="theme in availableThemes"
:key="theme.key"
:value="theme.key"
:class="{
'py-2 px-2 flex items-center cursor-pointer': true,
'text-sky-500 bg-gray-100 dark:bg-gray-600/30':
themeSetting === theme.key,
'hover:bg-gray-50 dark:hover:bg-gray-700/30':
themeSetting !== theme.key,
}"
>
<span class="text-sm mr-2 flex items-center">
<IconUil:sun v-if="theme.key === 'light'" />
<IconUil:moon v-else-if="theme.key === 'dark'" />
<IconUil:laptop v-else-if="theme.key === 'system'" />
<IconUil:clock v-else-if="theme.key === 'realtime'" />
</span>
{{ theme.text }}
</ListboxOption>
</ListboxOptions>
</Listbox>
<select
v-if="currentStyle === 'select-box'"
v-model="themeSetting"
class="w-full px-2 pr-3 py-1 outline-none rounded border bg-transparent text-gray-700 dark:text-gray-300 border-gray-900/10 dark:border-gray-50/[0.2]"
>
<option
v-for="theme in availableThemes"
:key="theme.key"
:value="theme.key"
>
{{ theme.text }}
</option>
</select>
</div>
</template>

View File

@@ -0,0 +1,3 @@
# Global Components
This directory make your components available globally.

19
components/global/Tab.vue Normal file
View File

@@ -0,0 +1,19 @@
<script lang="ts" setup>
defineProps({
name: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
})
const activeTab = inject<string>('activeTab')
</script>
<template>
<div v-show="activeTab === name" class="relative overflow-auto px-6 py-2">
<slot />
</div>
</template>

109
components/global/Tabs.vue Normal file
View File

@@ -0,0 +1,109 @@
<script lang="ts" setup>
// types
interface TabItem {
name: string
title: string
}
// composables
const slots = useSlots()
defineEmits(['click'])
// vars
const tabs = ref<HTMLDivElement>()
const tabHeaderIndicator = ref<HTMLDivElement>()
const tabItems = ref<TabItem[]>([])
const activeTab = ref<string>()
// provides
provide('activeTab', activeTab)
// methods
const updateIndicator = () => {
if (!tabs.value || !tabHeaderIndicator.value) return
// dom
const dom = tabHeaderIndicator.value
// get header tab item dom
const currentActiveIndex = tabItems.value.findIndex(
({ name }) => name === activeTab.value
)
const tabItem = tabs.value.querySelectorAll('.tabs-header-item')[
currentActiveIndex
] as HTMLDivElement
if (!tabItem) return
// set dom position and size to tab item
const padding = 24
const diff = 30
dom.style.width = `${tabItem.offsetWidth + diff}px`
dom.style.left = `${tabItem.offsetLeft - padding - diff / 2}px`
}
// watchs
watch(tabItems, () => updateIndicator())
watch(activeTab, () => updateIndicator())
// lifecycle
onBeforeMount(() => {
if (slots.default) {
slots.default().forEach((element, i) => {
const tab = element.props as TabItem
tabItems.value.push(tab)
if (i === 0) activeTab.value = tab.name
})
}
})
onMounted(() => {
;(async () => {
while (
typeof tabHeaderIndicator.value === 'undefined' ||
typeof tabs.value === 'undefined'
) {
await new Promise((resolve) => setTimeout(resolve, 10))
}
setTimeout(() => {
updateIndicator()
}, 350)
})()
})
</script>
<template>
<div ref="tabs" class="tabs">
<ClientOnly>
<div
class="tabs-header relative overflow-hidden flex space-x-6 text-sm font-bold text-gray-300 bg-primary-700/45 rounded-t-lg px-5 py-3"
>
<div
v-for="item in tabItems"
:key="item.name"
:class="{
'tabs-header-item': true,
'text-white': activeTab === item.name,
}"
:style="{
zIndex: 2,
}"
@click="activeTab = item.name"
>
<a href="#" @click.prevent="$emit('click')">
{{ item.title }}
</a>
</div>
<span
ref="tabHeaderIndicator"
class="absolute flex h-full top-0 left-0 p-1 py-1.5 overflow-hidden transition-all duration-300"
:style="{ zIndex: 1 }"
>
<span class="flex-1 bg-slate-500/40 rounded-lg" />
</span>
</div>
</ClientOnly>
<div
class="tabs-body relative text-slate-800 dark:text-white bg-gray-200 dark:bg-slate-800 shadow rounded-b-lg"
>
<slot />
</div>
</div>
</template>