package list from viandwi24

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

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_size = 2
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

5
.eslintignore Normal file
View File

@ -0,0 +1,5 @@
components.d.ts
nuxt.d.ts
dist
.nuxt
.output

18
.eslintrc Normal file
View File

@ -0,0 +1,18 @@
{
"root": true,
"env": {
"browser": true,
"node": true
},
"extends": [
"@nuxtjs/eslint-config-typescript",
"plugin:nuxt/recommended",
"plugin:prettier/recommended"
],
"plugins": [],
"rules": {
"vue/multi-word-component-names": "off",
"vue/no-multiple-template-root": "off",
"@typescript-eslint/no-unused-vars": "off"
}
}

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
*.log
nuxt.d.ts
# private config
.env
# deps
node_modules
# build or generate
.nuxt
dist
.output
# IDE
.idea
.vercel

4
.husky/commit-msg Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit ""

4
.husky/post-merge Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm install

4
.husky/pre-commit Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm lint-staged --no-stash

1
.npmrc Normal file
View File

@ -0,0 +1 @@
shamefully-hoist=true

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

4
.stackblitzrc Normal file
View File

@ -0,0 +1,4 @@
{
"installDependencies": true,
"startCommand": "npm run dev"
}

6
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"recommendations": [
"Vue.volar",
"Nuxt.mdc"
]
}

17
.vscode/markdown.code-snippets vendored Normal file
View File

@ -0,0 +1,17 @@
{
"Generate Nuxt 3 Content": {
"prefix": "n3:content",
"body": [
"---",
"title: '${1:${CONTENT_TITLE}}'",
"description: '${2:${CONTENT_DESCRIPTION}}'",
"date: '${3:${CONTENT_DATE}}'",
"author: '${4:${CONTENT_AUTHOR}}'",
"---",
"",
"${5:${CONTENT_BODY}}",
""
],
"description": "Generate Nuxt 3 Content"
}
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.tabSize": 2
}

31
.vscode/vue.code-snippets vendored Normal file
View File

@ -0,0 +1,31 @@
{
"Generate Nuxt 3 Page": {
"prefix": "n3:page",
"body": [
"<script lang=\"ts\" setup>",
"// compiler macro",
"definePageMeta({",
" layout: 'page',",
"})",
"useHead(() => ({",
" title: ${1:${PAGE_TITLE}},",
"}))",
"</script>",
"",
"<template>",
" <PageWrapper>",
" <PageHeader>",
" <PageTitle text=\"${1:${PAGE_TITLE}}\" class=\"capitalize\" />",
" </PageHeader>",
" <PageBody>",
" <PageSection>",
" <div>${2:${PAGE_BODY}}</div>",
" </PageSection>",
" </PageBody>",
" </PageWrapper>",
"</template>",
""
],
"description": "Generate Nuxt 3 Page"
}
}

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2022 Alfian Dwi Nugraha
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

264
README.md Normal file
View File

@ -0,0 +1,264 @@
# Nuxt 3 boilarplate
a Nuxt 3 starter template or boilerplate with a lot of useful features. and integrated with TailwindCSS 3.
_This template was built to make it easier to create web projects using Nuxt 3. It was originally designed for coursework and portfolio templates. (hence there will be lots of ui components for easy reuse)_
> **NOTES**
> - A new update is under development, you can see it [here](boilarplate/tree/v2)
> - This Project using "pnpm" as package manager. (not npm or yarn)!!!
> - Nuxt 3 now in stable version
> - Breaking changes tracker can be found [here](https://github.com/nuxt/framework/discussions/2883)
> - Roadmap can be found [here](https://v3.nuxtjs.org/community/roadmap)
## Features
- [x] 💨 [Tailwind CSS v3](https://tailwindcss.com/) with [Windicss](https://windicss.org/)
- [x] ✨ [Headless UI](https://headlessui.dev/)
- [x] 🔔 [Icon Pack Component (unplugin-icons)](https://icones.js.org/)
- [x] 🛹 [State & Store Management (Pinia)](https://pinia.vuejs.org/)
- [x] 🚩 [Localization (i18n) by @intlify](https://github.com/intlify/nuxt3) & Auto Generate Locales
- [x] 📦 [Vue Composition Collection (Vueuse)](https://vueuse.org/)
- [x] 📚 [Content Management System (Nuxt Content)](https://content.nuxtjs.org/) [SSR]
- [x] 🌙 Switch Theme (light, dark, system, realtime)
- [x] 🇮🇩 Language Switcher
- [x] 🪝 Built-in Component & Layout
- [x] Eslint & Prettier
- [x] Husky & Commitlint
- [x] Custom Workspace Snippets
- [x] Built-in Unit Test
- [x] Configurable Theme
- [x] Primary Colors
- [x] Font
## To Do
- [ ] Adding HTTP Client
## Preview New Major Update (Soon!)
<img src="boilarplate/blob/main/assets/images/preview_v2.png?raw=true" alt="Preview" title="Preview">
<p align="center">
<a href="boilarplate/tree/v2">Check here (branch v2)</a>
</p>
## Preview
<table align="center">
<tr>
<td align="center" width="100%" colspan="2">
<img src="boilarplate/blob/main/assets/images/preview_new.png?raw=true" alt="Preview" title="Preview">
</td>
</tr>
<tr>
<td align="center" width="75%">
<img src="boilarplate/blob/main/assets/images/preview.gif?raw=true" alt="Preview" title="Preview">
</td>
<td align="center" width="25%">
<img src="boilarplate/blob/main/assets/images/preview_mobile.gif?raw=true" alt="Preview" title="Preview">
</td>
</tr>
</table>
<p align="center">
<br>
<a href="https://nuxt3-awesome-starter.vercel.app/" target="_blank">
Live Demo
</a>
<br><br>
<a href="https://codesandbox.io/s/github/dwd/nuxt3-awesome-starter" title="Open In Code Sandbox">
<img src="https://img.shields.io/badge/Open%20in-CodeSandbox-blue?style=flat-square&logo=codesandboxg" alt="Open In Code Sandbox">
</a>
<br>
<a href="https://stackblitz.com/github/dwd/nuxt3-awesome-starter" title="Open In Stackblitz">
<img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open In Stackblitz">
</a>
</p>
## Table of Contents
- [Nuxt 3 boilarplate](#nuxt-3-awesome-starter)
- [Features](#features)
- [To Do](#to-do)
- [Preview](#preview)
- [Table of Contents](#table-of-contents)
- [Quick Start](#quick-start)
- [Start with this template](#start-with-this-template)
- [Deploy as Static Site](#deploy-as-static-site)
- [Built-in Components](#built-in-components)
- [Notes](#notes)
- [Nuxt Content](#nuxt-content)
- [Custom Workspace Snippets](#custom-workspace-snippets)
- [Styles](#styles)
- [Theme (Dark Mode)](#theme-dark-mode)
- [Localization](#localization)
- [Generate Locales](#generate-locales)
- [Icons](#icons)
- [Precommit and Postmerge](#precommit-and-postmerge)
- [License](#license)
## Quick Start
For detail information, go here [Getting Started](https://nuxt3-awesome-starter.vercel.app/getting-started)
### Start with this template
* This project using `pnpm` as package manager.
* Clone this project to your computer `git clone boilarplate`
* Install dependencies `pnpm install --shamefully-hoist`
* Run `pnpm dev` to start development server and open `http://localhost:3000` in your browser
### Deploy as Static Site
* Run `pnpm generate` to build the project
* Serve `dist/` folder
Checkout the [deployment documentation](https://v3.nuxtjs.org/docs/deployment).
## Built-in Components
- [x] Footer
- [x] Button
- [x] Anchor (link)
- [x] Alert
- [x] Card
- [x] Action Sheet
- [x] Theme Toggle / Switcher
- [x] Navbar
- [x] Navbar Builder
- [x] Drawer (on mobile)
- [x] Options (on mobile)
- [x] Page Layout
- [x] Wrapper
- [x] Header
- [x] Title
- [x] Body
- [x] Section
- [x] Section Wrapper
- [x] Section Title
- [x] Dashboard Layout
- [x] Sidebar
- [ ] Modal
## Notes
### Nuxt Content
With Nuxt Content, you can just create markdown file (recommended) inside `content` folder.
But this is only available for SSR (Server Side Rendering) mode. Static mode still not working, you can see the issue https://github.com/nuxt/content/issues/1202
For now, you can follow
### Custom Workspace Snippets
This workspace including custom snippets for VSCode.
- **n3:content**
content template with markdown
- **n3:page**
page template
### Styles
Tailwindcss import managed by windicss.
and you can add custom styles in :
```
/path/to/assets/sass/app.scss
```
### Theme (Dark Mode)
ThemeManager is a plugin that allows you to switch between themes. this lib in :
```
/path/to/utils/theme.ts
```
`Thememanager` is a function-class construct when app.vue before mounted. theme construct inside `AppSetup()` in `/path/to/app.vue` :
```vue
<!-- /path/to/app.vue -->
<script lang="ts" setup>
import { AppSetup } from '~/utils/app';
// app setup
AppSetup()
</script>
```
To change theme, you can direct set theme from state `theme.setting`, example :
```vue
<script lang="ts" setup>
import { IThemeSettingOptions } from '~/utils/theme'
const themeSetting = useState<IThemeSettingOptions>('theme.setting')
themeSetting.value = 'dark'
</script>
```
When you change state `theme.setting`, it will automatically change theme.
Theme Setting have 4 options :
- `light`
- `dark`
- `system` (operating system theme)
- `realtime` (realtime theme, if 05:00 - 17:00, it will change to light theme, otherwise dark)
We have state `theme.current`, this state return `light` or `dark` theme. basically it's process from `theme.setting`.
dont change theme with this state.
### Localization
Localization is a plugin that allows you to switch between languages. this lib in :
```
/path/to/utils/lang.ts
```
`LanguageManager` is a function-class construct when app.vue before mounted.
this lib depend on [@intlify/nuxt3](https://github.com/intlify/nuxt3)
lang construct inside `AppSetup()` in `/path/to/app.vue` :
<!-- /path/to/app.vue -->
<script lang="ts" setup>
import { AppSetup } from '~/utils/app';
// app setup
AppSetup()
</script>
To change language, you can direct set language from state `lang.setting`, example :
```vue
<script lang="ts" setup>
const langSetting = useState<string>('locale.setting')
langSetting.value = 'en'
</script>
```
When you change state `locale.setting`, it will automatically change language.
### Generate Locales
I made an automatic tool to automatically translate to all languages that have been prepared in the ./locales/ folder
So, you can just update "locales/en.yml" and run this tools, it will automatically translate to all languages.
You can just run :
```
pnpm generate:locales
# or :
node ./tools/translator.js ./locales en.yml
```
### Icons
This project using unplugin-icons for auto generate and import icon as component.
You can see collection icon list in : [https://icones.js.org/](https://icones.js.org/)
you can use `<prefix-collection:icon />` or `<PrefixCollection:Icon />`.
in this project, configuration prefix as a "icon", you can see in `nuxt.config.ts` :
```js
export default defineNuxtConfig({
...
vite: {
plugins: [
UnpluginComponentsVite({
dts: true,
resolvers: [
IconsResolver({
prefix: 'Icon',
}),
],
}),
],
},
...
})
```
Example :
```vue
// use icon from collection "Simple Icons" and name icon is "nuxtdotjs"
<IconSimpleIcons:nuxtdotjs />
// use icon from collection "Unicons" and name icon is "sun"
<IconUil:sun />
```
### Precommit and Postmerge
This project using husky and commitlint for precommit and postmerge.
when you commit, it will check your commit message and running "pnpm lint-staged" to check your staged files.
configuration in : `/path/to/.husky/pre-commit` and `/path/to/commitlint.config.js`
And when Postmerge, it will run "pnpm" to automatically install new dependencies.
configuration in `/path/to/.husky/post-merge`
## License
This project is licensed under the MIT license, Copyright (c) 2022 Alfian Dwi Nugraha. For more information see the [LICENSE](LICENSE.md) file.

7
app.config.ts Normal file
View File

@ -0,0 +1,7 @@
export default defineAppConfig({
name: 'Nuxt 3 boilarplate',
author: {
name: '',
link: '',
},
})

36
app.vue Normal file
View File

@ -0,0 +1,36 @@
<script lang="ts" setup>
import { AppConfigInput } from '@nuxt/schema'
import { AppSetup } from './utils/app'
import { ITheme } from './utils/theme'
AppSetup()
const theme = useState<ITheme>('theme.current')
const locale = useState<string>('locale.setting')
const app = useAppConfig() as AppConfigInput
useHead({
title: app.name,
titleTemplate: '%s - Nuxt 3 boilarplate',
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: 'Nuxt 3 boilarplate',
},
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
})
</script>
<template>
<Html :class="`${theme === 'dark' ? 'dark' : ''}`" :lang="locale">
<Body
class="antialiased duration-300 transition-colors text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-900 overscroll-y-none"
>
<NuxtLayout>
<NuxtLoadingIndicator :height="5" :duration="3000" :throttle="400" />
<NuxtPage />
</NuxtLayout>
</Body>
</Html>
</template>

BIN
assets/images/preview.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

View File

@ -0,0 +1 @@
$padding: 0.05em;

View File

@ -0,0 +1,25 @@
// page transition
.page-enter-active {
transition: all 0.1s ease-out;
}
.page-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.page-enter-from,
.page-leave-to {
transform: translateY(20px);
opacity: 0;
}
// layout transition
.layout-enter-active {
transition: all 0.1s ease-out;
}
.layout-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.layout-enter-from,
.layout-leave-to {
transform: translateY(-20px);
opacity: 0;
}

38
assets/sass/app.scss Normal file
View File

@ -0,0 +1,38 @@
@import url("https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,600;0,700;0,800;0,900;1,200;1,300;1,400;1,600;1,700;1,800;1,900&display=swap");
// vars
@import 'variables';
// components
::-webkit-scrollbar {
width: 16px;
}
::-webkit-scrollbar-track {
background: theme('colors.gray.100');
border-left: 1px solid theme('colors.gray.200');
}
::-webkit-scrollbar-thumb {
border: 4px solid theme('colors.gray.100');
background-clip: padding-box;
border-radius: 9999px;
background-color: theme('colors.slate.300');
}
::-webkit-scrollbar-thumb:hover {
background-color: theme('colors.slate.400');
}
html.dark {
::-webkit-scrollbar-track {
background: theme('colors.slate.800');
border-left: 1px solid theme('colors.slate.700');
}
::-webkit-scrollbar-thumb {
border-color: theme('colors.slate.800');
background-color: theme('colors.slate.500');
}
::-webkit-scrollbar-thumb:hover {
background-color: theme('colors.slate.400');
}
}
// animations
@import 'animations/transitions';

0
assets/sass/vendor.scss Normal file
View File

23
commitlint.config.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'chore',
'style',
'refactor',
'ci',
'test',
'revert',
'perf',
'build',
'vercel',
],
],
},
}

32
components.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
'Icon:ic:baselineArrowRightAlt': typeof import('~icons/ic/baseline-arrow-right-alt')['default']
'IconBi:exclamationCircleFill': typeof import('~icons/bi/exclamation-circle-fill')['default']
'IconClarity:timesCircleSolid': typeof import('~icons/clarity/times-circle-solid')['default']
'IconClarity:timesLine': typeof import('~icons/clarity/times-line')['default']
'IconFaSolid:ellipsisV': typeof import('~icons/fa-solid/ellipsis-v')['default']
'IconIc:baselineContentCopy': typeof import('~icons/ic/baseline-content-copy')['default']
'IconLa:language': typeof import('~icons/la/language')['default']
'IconMaterialSymbols:contentCopyOutline': typeof import('~icons/material-symbols/content-copy-outline')['default']
'IconMdi:checkCircle': typeof import('~icons/mdi/check-circle')['default']
'IconMdi:githubFace': typeof import('~icons/mdi/github-face')['default']
'IconSimpleIcons:nuxtdotjs': typeof import('~icons/simple-icons/nuxtdotjs')['default']
'IconUil:angleDown': typeof import('~icons/uil/angle-down')['default']
'IconUil:apps': typeof import('~icons/uil/apps')['default']
'IconUil:bars': typeof import('~icons/uil/bars')['default']
'IconUil:clock': typeof import('~icons/uil/clock')['default']
'IconUil:laptop': typeof import('~icons/uil/laptop')['default']
'IconUil:moon': typeof import('~icons/uil/moon')['default']
'IconUil:sun': typeof import('~icons/uil/sun')['default']
'IconUil:times': typeof import('~icons/uil/times')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

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>

8
composables/useLang.ts Normal file
View File

@ -0,0 +1,8 @@
import { useI18n } from 'vue-i18n'
export const useLang = () => {
const { t } = useI18n()
return {
t,
}
}

83
composables/useScreen.ts Normal file
View File

@ -0,0 +1,83 @@
export enum Size {
SMALL = 'sm',
MEDIUM = 'md',
LARGE = 'lg',
EXTRA_LARGE = 'xl',
}
export type ScreenSize =
| typeof Size.SMALL
| typeof Size.MEDIUM
| typeof Size.LARGE
| typeof Size.EXTRA_LARGE
export const defaultScreenConfig = {
[Size.SMALL]: 576,
[Size.MEDIUM]: 768,
[Size.LARGE]: 992,
[Size.EXTRA_LARGE]: 1200,
}
export const useScreen = () => {
const screenSize = reactive({
width: 0,
height: 0,
})
const current = ref<ScreenSize>(Size.SMALL)
const getSize = (width?: number) => {
if (width === undefined) width = screenSize.width
const {
[Size.SMALL]: sm,
[Size.MEDIUM]: md,
[Size.LARGE]: lg,
[Size.EXTRA_LARGE]: xl,
} = defaultScreenConfig
if (width < Number(sm)) return Size.SMALL
if (width < Number(md)) return Size.MEDIUM
if (width < Number(lg)) return Size.LARGE
if (width < Number(xl)) return Size.EXTRA_LARGE
return Size.EXTRA_LARGE
}
const onWindowResize = () => {
const { innerWidth, innerHeight } = window
screenSize.width = innerWidth
screenSize.height = innerHeight
current.value = getSize()
}
const higherThan = (size: ScreenSize) => {
const {
[Size.SMALL]: sm,
[Size.MEDIUM]: md,
[Size.LARGE]: lg,
[Size.EXTRA_LARGE]: xl,
} = defaultScreenConfig
const width = screenSize.width
if (size === Size.SMALL) return width >= Number(sm)
if (size === Size.MEDIUM) return width >= Number(md)
if (size === Size.LARGE) return width >= Number(lg)
if (size === Size.EXTRA_LARGE) return width >= Number(xl)
return false
}
onMounted(() => {
if (typeof window === 'undefined') return
window.addEventListener('resize', onWindowResize)
getSize(window.innerWidth)
})
onUnmounted(() => {
if (typeof window === 'undefined') return
window.removeEventListener('resize', onWindowResize)
})
return {
getSize,
screenSize,
current,
higherThan,
}
}

20
composables/useSticky.ts Normal file
View File

@ -0,0 +1,20 @@
export const useSticky = (el: HTMLElement, offset: number) => {
const onScroll = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
if (scrollTop > offset) {
el.classList.add('sticky')
} else {
el.classList.remove('sticky')
}
}
// lifecycle hooks
window.addEventListener('scroll', onScroll)
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
})
return {
onScroll,
}
}

View File

@ -0,0 +1,16 @@
import { WritableComputedRef } from 'vue'
export const useSyncProps = <T>(
props: any,
key: string,
emit: any
): WritableComputedRef<T> => {
return computed({
get() {
return props[key]
},
set(value) {
emit(`update:${key}`, value)
},
})
}

View File

@ -0,0 +1,58 @@
## Create a new project
:::div{class="mb-4"}
::tabs
:::tab{name="git" title="Git"}
```bash
$ git clone boilarplate
```
:::
::
:::
## Development
:::div{class="mb-4"}
::tabs
:::tab{name="pnpm" title="pnpm"}
```bash
# install dependencies
$ pnpm install --shamefully-hoist
# serve
$ pnpm dev
```
:::
::
:::
## Build
:::div{class="mb-4"}
::tabs
:::tab{name="pnpm" title="pnpm"}
```bash
# build
$ pnpm build
# serve
$ pnpm start
```
:::
::
:::
## Build as Static Site
:::div{class="mb-4"}
::tabs
:::tab{name="pnpm" title="pnpm"}
```bash
# generate static files
$ pnpm generate
# serve
$ pnpm preview
# or serve with "serve"
$ npx serve ./dist/
```
:::
::
:::

View File

@ -0,0 +1,41 @@
---
title: 'Hello World'
description: 'Hello !!!, this is a post about hello world hand demo Syntax Highlight Code.'
date: '2022-06-29'
author: 'dwd'
---
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
:::div{class="py-4"}
::tabs
:::tab{name="ts" title="TypeScript"}
```ts
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { enthusiasm } from './reducers/index';
import { StoreState } from './types/index';
import App from './components/App';
import './index.css';
const store = createStore<StoreState>(enthusiasm, {
enthusiasmLevel: 1,
languageName: 'TypeScript',
});
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
```
:::
::
:::

12
content/post/lorem.md Normal file
View File

@ -0,0 +1,12 @@
---
title: 'Lorem Post'
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
date: '2022-06-29'
author: 'dwd'
---
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.

8
content/post/test.md Normal file
View File

@ -0,0 +1,8 @@
---
title: 'CONTENT_TITLE'
description: 'CONTENT_DESCRIPTION'
date: '2022-06-29'
author: 'dwd'
---
CONTENT_BODY

25
global.d.ts vendored Normal file
View File

@ -0,0 +1,25 @@
import { IntlifyModuleOptions } from '@intlify/nuxt3'
import { VueUseNuxtOptions } from '@vueuse/nuxt'
import { ModuleOptions as NuxtWindiCssModuleOptions } from 'nuxt-windicss'
import { ModuleOptions as NuxtContentModuleOptions } from '@nuxt/content'
declare module '@nuxt/schema' {
interface AppConfigInput {
name: string
author: {
name: string
link: string
}
}
}
declare module 'nuxt/config' {
interface NuxtConfig {
intlify?: IntlifyModuleOptions
vueuse?: VueUseNuxtOptions
windicss?: NuxtWindiCssModuleOptions
content?: Partial<NuxtContentModuleOptions>
}
}
export {}

32
layouts/dashboard.vue Normal file
View File

@ -0,0 +1,32 @@
<template>
<div>
<slot name="app-before" />
<div id="app-before"></div>
<div class="flex flex-col min-h-screen">
<slot name="header">
<DashboardNavbar>
<template #drawer>
<DashboardSidebar mode="mobile" />
</template>
</DashboardNavbar>
</slot>
<div class="flex-1 w-full flex flex-col">
<div
class="relative flex-1 flex flex-row mx-auto max-w-8xl w-full h-full"
>
<div class="lg:pl-8 py-4">
<DashboardSidebar />
</div>
<div class="flex flex-col lg:ml-60 xl:ml-80">
<slot />
<slot name="footer">
<PageFooter />
</slot>
</div>
</div>
</div>
</div>
<slot name="app-after" />
<div id="app-after"></div>
</div>
</template>

23
layouts/page.vue Normal file
View File

@ -0,0 +1,23 @@
<template>
<div>
<slot name="app-before" />
<div id="app-before"></div>
<div class="flex flex-col min-h-screen">
<slot name="header">
<PageNavbar />
</slot>
<div class="flex-1 w-full flex flex-col">
<div
class="relative flex-1 flex flex-col mx-auto max-w-8xl w-full h-full"
>
<slot />
</div>
</div>
<slot name="footer">
<PageFooter />
</slot>
</div>
<slot name="app-after" />
<div id="app-after"></div>
</div>
</template>

72
locales/en.yml Normal file
View File

@ -0,0 +1,72 @@
components:
language_switcher:
change_language: change language
theme_switcher:
theme: theme
change_theme: change theme
pages:
index:
title: nuxt 3[]awesome[]starter
404:
title: page not found
blank:
nav: blank
title: blank page
description: this is a blank page
just_blank_page_with_title: just blank page with title
post:
nav: post
title: post
description: this is a post page
test:
nav: test
title: testing
description: this is a test page
counter: counter
increment: increment
decrement: decrement
reset: reset
identity: identity
full_name: fullName
getting-started:
nav: getting started
title: getting started
description: this is a getting started page
setting:
nav: setting
title: setting
description: this is a setting page
sections:
validate_username:
title: validate github profile
description: type your github username and click the button to validate.
footer: Learn more about
footer_button: validate
footer_link: github users api
bot_id:
title: bot id
description: This is your bot ID.
footer: Used when interacting with the bot.
protection_spam:
title: spam protection
description: toggle enable to remove the red border
footer: if enable we will secure your comments from spam
advanced_enable_advanced:
title: enable advanced settings
description: you can enable advanced settings to change the settings
advanced_dir_listing:
title: directory listing
description: if no index file is present within a directory, the directory contents will be displayed.
dashboard:
nav: dashboard
title: dashboard
index:
nav: home
title: home
banners:
welcome: hello, welcome to %{app_name}!
others:
learn_more: learn more
copy: copy
enabled: enabled
disabled: disabled

74
locales/id.yml Normal file
View File

@ -0,0 +1,74 @@
components:
language_switcher:
change_language: ganti bahasa
theme_switcher:
theme: tema
change_theme: ganti tema
pages:
'404':
title: halaman tidak ditemukan
index:
title: nuxt 3[]luar biasa[]pemula
blank:
nav: kosong
title: halaman kosong
description: ini adalah halaman kosong
just_blank_page_with_title: hanya halaman kosong dengan judul
post:
nav: pos
title: pos
description: ini adalah halaman posting
test:
nav: uji
title: pengujian
description: ini adalah halaman percobaan
counter: menangkal
increment: kenaikan
decrement: pengurangan
reset: mengatur ulang
identity: identitas
full_name: nama lengkap
getting-started:
nav: mulai
title: mulai
description: ini adalah halaman memulai
setting:
nav: pengaturan
title: pengaturan
description: ini adalah halaman pengaturan
sections:
validate_username:
title: validasi profil github
description: ketik nama pengguna github anda dan klik tombol untuk memvalidasi.
footer: belajar lebih tentang
footer_button: mengesahkan
footer_link: pengguna github api
bot_id:
title: id bot
description: ini adalah id bot anda.
footer: digunakan saat berinteraksi dengan bot.
protection_spam:
title: proteksi spam
description: alihkan aktifkan untuk menghapus batas merah
footer: jika memungkinkan kami akan mengamankan komentar anda dari spam
advanced_enable_advanced:
title: aktifkan pengaturan lanjutan
description: anda dapat mengaktifkan pengaturan lanjutan untuk mengubah pengaturan
advanced_dir_listing:
title: daftar direktori
description: >-
jika tidak ada file indeks dalam direktori, isi direktori akan
ditampilkan.
dashboard:
nav: dasbor
title: dasbor
index:
nav: rumah
title: rumah
banners:
welcome: halo, selamat datang di %{app_name}!
others:
learn_more: belajarlah lagi
copy: salinan
enabled: diaktifkan
disabled: dengan disabilitas

72
locales/ja.yml Normal file
View File

@ -0,0 +1,72 @@
components:
language_switcher:
change_language: 言語を変更
theme_switcher:
theme: テーマ
change_theme: テーマを変更
pages:
'404':
title: ページが見つかりません
index:
title: nuxt 3[]素晴らしい[]スターター
blank:
nav: 空欄
title: 空白ページ
description: これは空白のページです
just_blank_page_with_title: タイトルのある空白のページ
post:
nav: 役職
title: 役職
description: これは投稿ページです
test:
nav: テスト
title: テスト
description: これはテストページです
counter: カウンター
increment: インクリメント
decrement: デクリメント
reset: リセット
identity: 身元
full_name: フルネーム
getting-started:
nav: 入門
title: 入門
description: これは入門ページです
setting:
nav: 設定
title: 設定
description: 設定ページです
sections:
validate_username:
title: github プロファイルを検証する
description: github ユーザー名を入力し、ボタンをクリックして検証します。
footer: 詳しくはこちら
footer_button: 検証
footer_link: github ユーザー api
bot_id:
title: ボットid
description: これがボット id です。
footer: ボットと対話するときに使用されます。
protection_spam:
title: スパム保護
description: 有効に切り替えて、赤い枠を削除します
footer: 有効にすると、コメントをスパムから保護します
advanced_enable_advanced:
title: 詳細設定を有効にする
description: 詳細設定を有効にして設定を変更できます
advanced_dir_listing:
title: ディレクトリ一覧
description: ディレクトリ内にインデックス ファイルが存在しない場合は、ディレクトリの内容が表示されます。
dashboard:
nav: ダッシュボード
title: ダッシュボード
index:
nav:
title:
banners:
welcome: こんにちは、%{app_name} へようこそ!
others:
learn_more: もっと詳しく知る
copy: コピー
enabled: 有効
disabled: 無効

72
locales/ko.yml Normal file
View File

@ -0,0 +1,72 @@
components:
language_switcher:
change_language: 언어 변경
theme_switcher:
theme: 주제
change_theme: 테마 변경
pages:
'404':
title: 페이지를 찾을 수 없습니다
index:
title: nuxt 3[]멋진[]스타터
blank:
nav: 공백
title: 빈 페이지
description: 이것은 빈 페이지입니다
just_blank_page_with_title: 제목이 있는 빈 페이지
post:
nav: 게시하다
title: 게시하다
description: 이것은 게시물 페이지입니다
test:
nav: 테스트
title: 테스트
description: 이것은 테스트 페이지입니다
counter: 카운터
increment: 증가
decrement: 감소
reset: 초기화
identity: 신원
full_name: 전체 이름
getting-started:
nav: 시작하기
title: 시작하기
description: 이것은 시작 페이지입니다
setting:
nav: 환경
title: 환경
description: 이것은 설정 페이지입니다
sections:
validate_username:
title: github 프로필 확인
description: github 사용자 이름을 입력하고 버튼을 클릭하여 확인하십시오.
footer: 에 대해 자세히 알아보기
footer_button: 확인
footer_link: github 사용자 api
bot_id:
title: 봇 아이디
description: 이것은 봇 id입니다.
footer: 봇과 상호 작용할 때 사용됩니다.
protection_spam:
title: 스팸 방지
description: 활성화를 토글하여 빨간색 테두리를 제거합니다.
footer: 활성화하면 스팸으로부터 댓글을 보호합니다.
advanced_enable_advanced:
title: 고급 설정 활성화
description: 고급 설정을 활성화하여 설정을 변경할 수 있습니다
advanced_dir_listing:
title: 디렉토리 목록
description: 디렉토리 내에 인덱스 파일이 없으면 디렉토리 내용이 표시됩니다.
dashboard:
nav: 계기반
title: 계기반
index:
nav:
title:
banners:
welcome: 안녕하세요, %{app_name}에 오신 것을 환영합니다!
others:
learn_more: 더 알아보기
copy: 복사
enabled: 활성화
disabled: 장애가 있는

72
locales/tr.yml Normal file
View File

@ -0,0 +1,72 @@
components:
language_switcher:
change_language: dili değiştir
theme_switcher:
theme: tema
change_theme: temayı değiştir
pages:
index:
title: nuxt 3[]mükemmel[]başlangıç
404:
title: sayfa bulunamadı
blank:
nav: boş
title: boş sayfa
description: bu bir boş sayfa
just_blank_page_with_title: başlıklı boş sayfa
post:
nav: paylaşım
title: paylaşım
description: bu bir paylaşım sayfası
test:
nav: test
title: test
description: bu bir test sayfası
counter: sayaç
increment: arttır
decrement: azalt
reset: sıfırla
identity: kimlik
full_name: Ad Soyad
getting-started:
nav: başlangıç
title: başlangıç
description: bu bir başlangıç sayfasıdır
setting:
nav: ayarlar
title: ayarlar
description: bu bir ayar sayfasıdır
sections:
validate_username:
title: github profilini doğrula
description: github kullanıcı adınızı yazın ve doğrulamak için düğmeye tıklayın.
footer: hakkımızda daha fazlasını öğrenin
footer_button: doğrula
footer_link: github kullanıcı apisi
bot_id:
title: bot id
description: Bu sizin bot ID'niz.
footer: Botla etkileşime girerken kullanılır.
protection_spam:
title: spam koruması
description: kırmızı kenarlığı kaldırmak için etkinleştir
footer: etkinleştirirseniz, yorumlarınızı spam'dan koruyacağız
advanced_enable_advanced:
title: gelişmiş ayarları etkinleştir
description: ayarları değiştirmek için gelişmiş ayarları etkinleştirebilirsiniz
advanced_dir_listing:
title: dizin listesi
description: bir dizinde dizin dosyası yoksa, dizin içeriği görüntülenecektir.
dashboard:
nav: kontrol paneli
title: kontrol paneli
index:
nav: ana sayfa
title: ana sayfa
banners:
welcome: merhaba, %{app_name}'a hoşgeldiniz!
others:
learn_more: daha fazla bilgi edin
copy: kopyala
enabled: aktif
disabled: aktif değil

72
locales/zh.yml Normal file
View File

@ -0,0 +1,72 @@
components:
language_switcher:
change_language: 切换语言
theme_switcher:
theme: 主题
change_theme: 切换主题
pages:
index:
title: nuxt 3[]awesome[]starter
404:
title: 找不到该页面
blank:
nav: 空白
title: 空白页
description: 这是一个空白页
just_blank_page_with_title: 只是带有标题的空白页
post:
nav: 文章
title: 文章
description: 这是一个文章页面
test:
nav: 测试
title: 测试
description: 这是一个测试页面
counter: 计数器
increment: 增加
decrement: 减少
reset: 重置
identity: 标识
full_name: 全名
getting-started:
nav: 快速开始
title: 快速开始
description: 这是一个快速开始页面
setting:
nav: 设置
title: 设置
description: 这是一个设置页面
sections:
validate_username:
title: 验证 github 配置文件
description: 输入您的 github 用户名,然后单击按钮进行验证。
footer: 了解更多
footer_button: 验证
footer_link: github 用户 api
bot_id:
title: 机器人 id
description: 这是你的机器人 ID.
footer: 在与机器人交互时使用。
protection_spam:
title: 垃圾邮件防护
description: 切换启用以删除红色边框
footer: 如果启用,我们将保护您的评论免受垃圾邮件的侵害
advanced_enable_advanced:
title: 高级设置
description: 您可以启用高级设置以更改设置
advanced_dir_listing:
title: 目录列表
description: 如果目录中不存在索引文件,则将显示目录内容。
dashboard:
nav: 仪表盘
title: 仪表盘
index:
nav: 主页
title: 主页
banners:
welcome: 你好, 欢迎来到 %{app_name}!
others:
learn_more: 了解更多
copy: 复制
enabled: 启用
disabled: 禁用

104
nuxt.config.ts Normal file
View File

@ -0,0 +1,104 @@
import UnpluginComponentsVite from 'unplugin-vue-components/vite'
import IconsResolver from 'unplugin-icons/resolver'
// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
// server side rendering mode
ssr: true,
// typescripts
typescript: {
strict: true,
typeCheck: true,
},
// css
css: ['~/assets/sass/vendor.scss', '~/assets/sass/app.scss'],
// plugins
plugins: ['~/plugins/navbar.ts'],
// build
build: {
transpile: ['@headlessui/vue'],
},
// modules
modules: [
'unplugin-icons/nuxt',
'@intlify/nuxt3',
'@pinia/nuxt',
'@nuxt/content',
'@vueuse/nuxt',
'nuxt-windicss',
],
// experimental features
experimental: {
reactivityTransform: false,
},
// auto import components
components: true,
// vite plugins
vite: {
plugins: [
UnpluginComponentsVite({
dts: true,
resolvers: [
IconsResolver({
prefix: 'Icon',
}),
],
}),
],
},
// app config
app: {
// global transition
pageTransition: { name: 'page', mode: 'out-in' },
layoutTransition: { name: 'layout', mode: 'out-in' },
},
// localization - i18n config
intlify: {
localeDir: 'locales',
vueI18n: {
locale: 'en',
fallbackLocale: 'en',
availableLocales: ['en', 'id', 'ja', 'ko'],
},
},
// vueuse
vueuse: {
ssrHandlers: true,
},
// windicss
windicss: {
analyze: {
analysis: {
interpretUtilities: false,
},
server: {
port: 4000,
open: false,
},
},
scan: true,
},
// content
content: {
documentDriven: true,
markdown: {
mdc: true,
},
highlight: {
theme: 'github-dark',
},
},
})

74
package.json Normal file
View File

@ -0,0 +1,74 @@
{
"name": "nuxt3-awesome-starter",
"version": "0.1.0",
"description": "a Nuxt 3 starter template or boilerplate with a lot of useful features. Nuxt3 + Tailwindcss 3",
"repository": {
"type": "git",
"url": "git://github.com/dwd/nuxt3-awesome-starter"
},
"author": "dwd",
"license": "MIT",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"start": "node .output/server/index.mjs",
"serve": "serve dist/",
"postinstall": "husky install",
"lint": "eslint --ext \".ts,.js,.vue\" --ignore-path .eslintignore .",
"lintfix": "eslint --fix --ext \".ts,.js,.vue\" --ignore-path .eslintignore .",
"prepare": "husky install",
"clean": "rm -rf .nuxt dist .output",
"generate:locales": "node tools/translator.js ./locales en.yml"
},
"devDependencies": {
"@babel/core": "^7.20.2",
"@commitlint/cli": "^17.2.0",
"@commitlint/config-conventional": "^17.2.0",
"@headlessui/vue": "^1.7.8",
"@iconify/json": "^2.1.135",
"@intlify/nuxt3": "^0.2.4",
"@nuxt/content": "^2.4.2",
"@nuxt/test-utils-edge": "3.1.1-rc.0-27911047.7d812db",
"@nuxtjs/eslint-config": "^12.0.0",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
"@pinia/nuxt": "^0.4.6",
"@vueuse/core": "^9.11.1",
"@vueuse/nuxt": "^9.11.1",
"eslint": "^8.32.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-nuxt": "^3.2.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.2",
"js-yaml": "^4.1.0",
"lint-staged": "^13.0.3",
"nuxt": "3.1.1",
"nuxt-windicss": "^2.6.0",
"pinia": "^2.0.29",
"postcss": "8.4.14",
"postcss-loader": "^7.0.0",
"prettier": "^2.7.1",
"sass": "1.53.0",
"sass-loader": "^13.2.0",
"serve": "^13.0.2",
"three": "^0.143.0",
"translate": "^1.4.1",
"typescript": "^4.9.4",
"unplugin-icons": "^0.14.14",
"unplugin-vue-components": "^0.22.9",
"vite": ">=2.9.0 <3.0.0 || >=3.0.0-0 <3.0.0",
"vitest": "^0.28.3",
"vue": "^3.2.41",
"vue-tsc": "^1.0.9",
"webpack": ">=5.0.0 <6.0.0"
},
"lint-staged": {
"**/*.{js,ts,vue,html}": [
"pnpm lintfix"
]
},
"dependencies": {
"@nuxt/ui": "^0.4.1"
}
}

20
pages/404.vue Normal file
View File

@ -0,0 +1,20 @@
<script lang="ts" setup>
import { capitalize } from '~/utils/str'
// composable
const { t } = useLang()
// compiler macro
definePageMeta({
layout: 'page',
})
useHead(() => ({
title: capitalize(t('pages.404.title')),
}))
</script>
<template>
<PageWrapper class="flex flex-col items-center justify-center">
<Error :code="404" />
</PageWrapper>
</template>

35
pages/blank.vue Normal file
View File

@ -0,0 +1,35 @@
<script lang="ts" setup>
import { capitalize } from '~/utils/str'
// composable
const { t } = useLang()
// compiler macro
definePageMeta({
layout: 'page',
})
useHead(() => ({
title: capitalize(t('pages.blank.title')),
meta: [
{
name: 'description',
content: t('pages.blank.description'),
},
],
}))
</script>
<template>
<PageWrapper>
<PageHeader>
<PageTitle :text="$t('pages.blank.title')" class="capitalize" />
</PageHeader>
<PageBody>
<PageSection>
<div v-for="i in 30" :key="i" class="text-6xl uppercase">
{{ $t('pages.blank.just_blank_page_with_title') }}
</div>
</PageSection>
</PageBody>
</PageWrapper>
</template>

27
pages/dashboard/index.vue Normal file
View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
definePageMeta({
layout: 'dashboard',
})
</script>
<template>
<PageWrapper>
<PageHeader>
<PageTitle :text="$t('pages.dashboard.index.title')" class="capitalize" />
</PageHeader>
<PageBody>
<PageSection>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia autem
debitis ab dolorum tempore placeat possimus perferendis porro sit aut
nobis quasi hic consequuntur, atque impedit nihil totam illo odit?
</p>
</PageSection>
<PageSection>
<div v-for="i in 30" :key="i" class="text-6xl uppercase">
{{ $t('pages.blank.just_blank_page_with_title') }}
</div>
</PageSection>
</PageBody>
</PageWrapper>
</template>

27
pages/getting-started.vue Normal file
View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import { capitalize } from '~/utils/str'
// composable
const { t } = useLang()
// compiler macro
definePageMeta({
layout: 'page',
})
useHead(() => ({
title: capitalize(t('pages.getting-started.title')),
meta: [
{
name: 'description',
content: t('pages.getting-started.description'),
},
],
}))
</script>
<template>
<PageContentRenderer
path="/page/getting-started"
:page-title="$t('pages.getting-started.title')"
/>
</template>

283
pages/index.vue Normal file
View File

@ -0,0 +1,283 @@
<script lang="ts" setup>
// composable
const { t } = useLang()
// meta
definePageMeta({
layout: 'page',
})
// vars
const titlesText = computed<string[]>(() => t('pages.index.title').split('[]'))
const leadingsText = computed(() => [
{
text: titlesText.value[0],
startColor: '#007CF0',
endColor: '#00DFD8',
delay: 0,
},
{
text: titlesText.value[1],
startColor: '#7928CA',
endColor: '#FF0080',
delay: 2,
},
{
text: titlesText.value[2],
startColor: '#FF4D4D',
endColor: '#F9CB28',
delay: 4,
},
])
const tooltip = ref(false)
// const
const cancelTooltip = () => {
tooltip.value = false
const tt = document.querySelector('.tooltiptext')
if (tt) tt.innerHTML = `Copy to clipboard`
}
const copyBash = () => {
const bash = 'git clone boilarplate'
navigator.clipboard.writeText(bash)
tooltip.value = true
const tt = document.querySelector('.tooltiptext')
if (tt) tt.innerHTML = `Copied!!!`
}
</script>
<template>
<PageWrapper class="flex-1 flex">
<div class="background-overlay">
<div
class="absolute top-0 left-0 transform translate-x-64 translate-y-4 h-14 w-14 rounded-full bg-gray-900 dark:bg-white"
></div>
<div
class="absolute hidden md:block top-0 left-0 transform translate-x-18 translate-y-20 h-28 w-28 rounded-full bg-blue-600 linear-wipe"
></div>
<div
class="absolute hidden md:block bottom-0 right-0 transform -translate-x-4 -translate-y-40 h-16 w-16 rounded bg-purple-600 linear-wipe"
></div>
<div class="absolute bottom-0 right-0 triangle-shape"></div>
</div>
<PageBody class="flex-1 flex">
<PageSection class="flex-1 flex items-center">
<div class="flex-1 md:w-5/8 flex flex-col z-10">
<h1 class="text-center md:text-left mt-4">
<span
v-for="(item, i) in leadingsText"
:key="i"
:style="`--content: '${item.text}'; --start-color: ${
item.startColor
}; --end-color: ${item.endColor}; --animation-name: anim-fg-${
i + 1
}`"
class="animated-text-bg drop-shadow-xl text-5xl xl:text-8xl 2xl:text-9xl block font-black uppercase"
>
<span class="animated-text-fg">{{ item.text }}</span>
</span>
</h1>
<div
class="flex space-x-4 ml-4 mt-10 justify-center md:justify-start"
>
<Button
size="lg"
text="Nuxt 3"
class="font-extrabold"
href="https://v3.nuxtjs.org"
/>
<Button
size="lg"
text="Github"
type="secondary"
class="font-extrabold"
href="boilarplate"
/>
</div>
</div>
<div class="hidden md:flex md:w-3/8 justify-center items-end relative">
<Gem class="absolute -top-64 -right-0" />
<div class="ml-4 w-100 z-10 h-auto shadow">
<div
class="win-header bg-gray-200 dark:bg-slate-800 flex flex space-x-4 px-3 py-2 rounded-t-lg relative border-b-2 border-gray-300/75 dark:border-slate-700/75"
>
<div class="win-controls flex space-x-1 items-center">
<div class="w-3 h-3 bg-red-500 rounded-full" />
<div class="w-3 h-3 bg-green-500 rounded-full" />
<div class="w-3 h-3 bg-yellow-500 rounded-full" />
</div>
<div class="flex-1 font-bold text-center pr-12 text-sm">BASH</div>
<div class="text-sm flex justify-center items-center">
<div class="tooltip">
<button
class="text-gray-100 flex justify-center items-center"
@click="copyBash"
@mouseout="cancelTooltip"
>
<span class="tooltiptext">Copy to clipboard</span>
<icon-material-symbols:content-copy-outline />
</button>
</div>
</div>
</div>
<div
class="win-body rounded-b-lg bg-gray-200/90 dark:bg-slate-800/90 px-3 py-2 font-mono backdrop-filter backdrop-blur-lg"
>
<div>
$ git clone boilarplate
</div>
</div>
</div>
</div>
</PageSection>
</PageBody>
</PageWrapper>
</template>
<style lang="scss">
@import '../assets/sass/variables';
@keyframes anim-fg-1 {
0%,
16.667%,
100% {
opacity: 1;
}
33.333%,
83.333% {
opacity: 0;
}
}
@keyframes anim-fg-2 {
0%,
16.667%,
66.667%,
100% {
opacity: 0;
}
33.333%,
50% {
opacity: 1;
}
}
@keyframes anim-fg-3 {
0%,
50%,
100% {
opacity: 0;
}
66.667%,
83.333% {
opacity: 1;
}
}
.animated-text-bg {
position: relative;
display: block;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
content: var(--content);
display: block;
width: 100%;
color: theme('colors.slate.800');
top: 0;
bottom: 0;
left: 0;
z-index: 0;
padding-left: $padding;
padding-right: $padding;
&:before {
content: var(--content);
position: absolute;
display: block;
width: 100%;
color: theme('colors.slate.800');
top: 0;
bottom: 0;
left: 0;
z-index: 0;
padding-left: $padding;
padding-right: $padding;
}
}
.animated-text-fg {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
padding-left: $padding;
padding-right: $padding;
background-image: linear-gradient(
90deg,
var(--start-color),
var(--end-color)
);
position: relative;
opacity: 0;
z-index: 1;
animation: var(--animation-name) 8s infinite;
}
html.dark {
.animated-text-bg {
color: theme('colors.gray.100');
&:before {
color: theme('colors.gray.100');
}
}
}
.triangle-shape {
width: 0;
height: 0;
border-left: 25px solid transparent;
border-right: 25px solid transparent;
border-bottom: 40px solid theme('colors.green.600');
transform: translate(-15rem, -6rem) rotate(45deg);
}
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 140px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 150%;
left: 50%;
margin-left: -75px;
opacity: 0;
transition: opacity 0.3s;
}
.tooltip .tooltiptext::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #555 transparent transparent transparent;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
</style>

11
pages/post/[slug].vue Normal file
View File

@ -0,0 +1,11 @@
<script lang="ts" setup>
definePageMeta({
layout: 'page',
})
</script>
<template>
<PageWrapper class="flex flex-col">
<PageContentDoc empty-tip="Post im empty" />
</PageWrapper>
</template>

67
pages/post/index.vue Normal file
View File

@ -0,0 +1,67 @@
<script lang="ts" setup>
import { capitalize } from '~/utils/str'
// composable
const { t } = useLang()
// compiler macro
definePageMeta({
layout: 'page',
})
useHead(() => ({
title: capitalize(t('pages.post.title')),
meta: [
{
name: 'description',
content: t('pages.post.description'),
},
],
}))
</script>
<template>
<PageWrapper>
<PageHeader>
<PageTitle :text="$t('pages.post.title')" class="capitalize" />
</PageHeader>
<PageBody>
<ContentList v-slot="{ list }" path="/post">
<PageSection v-for="article in list" :key="article._path">
<div
class="block hover:no-underline p-6 flex space-x-6 rounded border border-gray-900/10 dark:border-gray-50/[0.2]"
>
<div class="mt-1 text-slate-600 dark:text-gray-400 text-right">
<div>{{ article.date }}</div>
<Anchor
class="text-sm flex items-center justify-end space-x-1"
:href="`https://www.github.com/${article.author}`"
>
<icon-mdi:github-face class="text-xs" />
<span>{{ article.author }}</span>
</Anchor>
</div>
<div class="flex flex-col">
<div
class="text-xl font-semibold text-slate-800 dark:text-gray-50"
>
{{ article.title }}
</div>
<div class="text-slate-700 dark:text-gray-300 mb-1">
{{ article.description }}
</div>
<div class="flex">
<Anchor
class="text-sm flex space-x-1 items-center text-primary-500"
:to="article._path"
>
<span>{{ $t('others.learn_more') }}</span>
<icon:ic:baseline-arrow-right-alt class="text-sm" />
</Anchor>
</div>
</div>
</div>
</PageSection>
</ContentList>
</PageBody>
</PageWrapper>
</template>

301
pages/setting.vue Normal file
View File

@ -0,0 +1,301 @@
<script lang="ts" setup>
import {
TabGroup,
TabList,
Tab as HeadlessUiTab,
TabPanels,
TabPanel,
} from '@headlessui/vue'
import { capitalize } from '~/utils/str'
import { Size } from '~/composables/useScreen'
// composable
const { t } = useLang()
const screen = useScreen()
// compiler macro
definePageMeta({
layout: 'page',
})
useHead(() => ({
title: capitalize(t('pages.setting.title')),
meta: [
{
name: 'description',
content: t('pages.setting.description'),
},
],
}))
// funcs
const randomToken = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let token = ''
for (let i = 0; i < 255; i++) {
token += chars.charAt(Math.floor(Math.random() * chars.length))
}
return token
}
// state
const username = ref('dwd')
const id = ref(randomToken())
const enableSpamProtection = ref(false)
const enableDirList = ref(false)
const enableAdvancedSetting = ref(false)
// methods
const validate = async () => {
// fetch username from github api
try {
const response = await fetch(
`https://api.github.com/users/${username.value}`
)
if (response.status !== 200)
throw new Error(
`error when fetching username : ${response.statusText} (${response.status})`
)
const data = (await response.json()) as {
name: string
id: string
}
alert(`Found Accout Name ${data.name} with id : ${data.id}`)
} catch (err) {
alert(err)
}
}
</script>
<template>
<PageWrapper>
<PageSection class="mb-0">
<Alert
type="success"
title="This is a page for testing purposes"
text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
class="mb-6"
/>
</PageSection>
<PageHeader>
<PageTitle :text="$t('pages.setting.title')" class="capitalize" />
</PageHeader>
<PageBody>
<PageSection>
<TabGroup
as="div"
class="flex flex-col md:flex-row md:space-x-4"
:vertical="screen.higherThan(Size.MEDIUM)"
>
<TabList class="w-full md:w-1/6 flex md:flex-col rounded-lg mb-2">
<HeadlessUiTab v-slot="{ selected }" as="template">
<button
:class="[
'md:w-full text-left px-3 py-1.5 rounded py-2.5 text-sm leading-5 transition-all hover:bg-gray-200 hover:text-slate-900 dark:hover:bg-white/[0.12] dark:hover:text-white',
selected
? 'font-extrabold'
: 'text-slate-800 dark:text-gray-400',
]"
>
General
</button>
</HeadlessUiTab>
<HeadlessUiTab v-slot="{ selected }" as="template">
<button
:class="[
'md:w-full text-left px-3 py-1.5 rounded py-2.5 text-sm leading-5 transition-all hover:bg-gray-200 hover:text-slate-900 dark:hover:bg-white/[0.12] dark:hover:text-white',
selected
? 'font-extrabold'
: 'text-slate-800 dark:text-gray-400',
]"
>
Protection
</button>
</HeadlessUiTab>
<HeadlessUiTab v-slot="{ selected }" as="template">
<button
:class="[
'md:w-full text-left px-3 py-1.5 rounded py-2.5 text-sm leading-5 transition-all hover:bg-gray-200 hover:text-slate-900 dark:hover:bg-white/[0.12] dark:hover:text-white',
selected
? 'font-extrabold'
: 'text-slate-800 dark:text-gray-400',
]"
>
Advanced
</button>
</HeadlessUiTab>
</TabList>
<TabPanels class="flex-1">
<TabPanel>
<Card class="mb-4">
<CardContent>
<CardTitle
class="capitalize"
:text="$t('pages.setting.sections.validate_username.title')"
/>
<p class="mb-2">
{{
$t('pages.setting.sections.validate_username.description')
}}
</p>
<div class="flex">
<FormTextInput v-model="username" class="w-full md:w-1/3">
<template #prefix-disabled>
<span class="flex-1 px-4 py-2">github.com/</span>
</template>
</FormTextInput>
</div>
</CardContent>
<CardFooter
class="flex flex-col space-y-2 md:space-y md:flex-row items-center md:justify-between"
>
<p>
{{ $t('pages.setting.sections.validate_username.footer') }}
<Anchor
class="underline font-bold capitalize"
:text="
$t(
'pages.setting.sections.validate_username.footer_link'
)
"
href="https://docs.github.com/en/rest/users/users#get-a-user"
/>
</p>
<Button
class="capitalize"
size="sm"
type="opposite"
:text="
$t(
'pages.setting.sections.validate_username.footer_button'
)
"
@click="validate"
/>
</CardFooter>
</Card>
<Card class="mb-4">
<CardContent>
<CardTitle
class="capitalize"
:text="$t('pages.setting.sections.bot_id.title')"
/>
<p class="mb-2">
{{ $t('pages.setting.sections.bot_id.description') }}
</p>
<div class="flex">
<FormTextInput v-model="id" class="w-full md:w-1/3">
<template #suffix>
<Button
type="opposite"
class="flex space-x-1 border-none"
>
<icon-ic:baseline-content-copy />
<span>{{ $t('others.copy') }}</span>
</Button>
</template>
</FormTextInput>
</div>
</CardContent>
<CardFooter class="justify-between">
<p>
{{ $t('pages.setting.sections.bot_id.footer') }}
</p>
</CardFooter>
</Card>
</TabPanel>
<TabPanel>
<Card
:class="{
'mb-4': true,
'border-red-500 dark:border-red-500': !enableSpamProtection,
}"
>
<CardContent>
<CardTitle
class="capitalize"
:text="$t('pages.setting.sections.protection_spam.title')"
/>
<p class="mb-2">
{{
$t('pages.setting.sections.protection_spam.description')
}}
</p>
<div class="flex">
<FormSwitch v-model="enableSpamProtection">
<span class="capitalize">{{
enableSpamProtection
? $t('others.enabled')
: $t('others.disabled')
}}</span>
</FormSwitch>
</div>
</CardContent>
<CardFooter class="justify-between">
<p>
{{ $t('pages.setting.sections.protection_spam.footer') }}
</p>
</CardFooter>
</Card>
</TabPanel>
<TabPanel>
<Card class="mb-4">
<CardContent>
<CardTitle
class="capitalize"
:text="
$t(
'pages.setting.sections.advanced_enable_advanced.title'
)
"
/>
<p class="mb-2">
{{
$t(
'pages.setting.sections.advanced_enable_advanced.description'
)
}}
</p>
<div class="flex">
<FormSwitch v-model="enableAdvancedSetting">
<span class="capitalize">{{
enableAdvancedSetting
? $t('others.enabled')
: $t('others.disabled')
}}</span>
</FormSwitch>
</div>
</CardContent>
</Card>
<Card class="mb-4" :disabled="!enableAdvancedSetting">
<CardContent>
<CardTitle
class="capitalize"
:text="
$t('pages.setting.sections.advanced_dir_listing.title')
"
/>
<p class="mb-2">
{{
$t(
'pages.setting.sections.advanced_dir_listing.description'
)
}}
</p>
<div class="flex">
<FormSwitch v-model="enableDirList" on>
<span class="capitalize">{{
enableDirList
? $t('others.enabled')
: $t('others.disabled')
}}</span>
</FormSwitch>
</div>
</CardContent>
</Card>
</TabPanel>
</TabPanels>
</TabGroup>
</PageSection>
</PageBody>
</PageWrapper>
</template>

106
pages/test.vue Normal file
View File

@ -0,0 +1,106 @@
<script lang="ts" setup>
import { useCounter } from '~/stores/counter'
import { useIdentity } from '~/stores/identity'
import { capitalize } from '~/utils/str'
// composable
const { t } = useLang()
// compiler macro
definePageMeta({
layout: 'page',
})
useHead(() => ({
title: capitalize(t('pages.test.title')),
meta: [
{
name: 'description',
content: t('pages.test.description'),
},
],
}))
const counter = useCounter()
const identity = useIdentity()
</script>
<template>
<PageWrapper>
<PageHeader>
<PageTitle :text="$t('pages.test.title')" class="capitalize" />
</PageHeader>
<PageBody>
<PageSection>
<PageSectionTitle :text="$t('pages.test.counter')" class="capitalize" />
<div class="">
<div class="mb-2">Counter : {{ counter.count }}</div>
<div
class="flex flex-col items-center justify-items-center space-y-2 md:space-y-0 md:flex-row md:space-x-2"
>
<Button
class="w-full md:w-auto capitalize"
type="secondary"
size="sm"
:text="$t('pages.test.increment')"
@click.prevent="counter.increment"
/>
<Button
class="w-full md:w-auto"
type="secondary"
size="sm"
:text="`${$t('pages.test.increment')} 2x`"
@click.prevent="counter.increment2x"
/>
<Button
class="w-full md:w-auto capitalize"
type="secondary"
size="sm"
:text="$t('pages.test.decrement')"
@click.prevent="counter.decrement"
/>
<Button
class="w-full md:w-auto capitalize"
type="secondary"
size="sm"
:text="$t('pages.test.reset')"
@click.prevent="counter.reset"
/>
</div>
</div>
</PageSection>
<PageSection>
<PageSectionTitle
:text="$t('pages.test.identity')"
class="capitalize"
/>
<div class="mb-2">
<span class="capitalize">{{ $t('pages.test.full_name') }} : </span>
<span>{{ identity.fullName }}</span>
</div>
<div class="mb-2">
<div
class="flex flex-col items-center space-y-2 md:space-y-0 md:flex-row md:space-x-2"
>
<FormTextInput
v-model="identity.firstName"
size="md"
class="w-full md:w-1/3"
/>
<FormTextInput
v-model="identity.lastName"
size="md"
class="w-full md:w-1/3"
/>
<Button
class="capitalize w-full md:w-auto"
:text="$t('pages.test.reset')"
type="secondary"
size="md"
@click.prevent="identity.reset"
/>
</div>
</div>
</PageSection>
</PageBody>
</PageWrapper>
</template>

9
plugins/navbar.ts Normal file
View File

@ -0,0 +1,9 @@
export default defineNuxtPlugin((nuxtApp) => {
// when page redirect on mobile device, close drawer navbar
nuxtApp.hook('page:finish', () => {
const showDrawer = useState<boolean>('navbar.showDrawer', () => false)
const showOptions = useState<boolean>('navbar.showOptions', () => false)
showDrawer.value = false
showOptions.value = false
})
})

11732
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

233
public/assets/gem/gem.gltf Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

9
sandbox.config.json Normal file
View File

@ -0,0 +1,9 @@
{
"infiniteLoopProtection": true,
"hardReloadOnChange": false,
"view": "browser",
"template": "nuxt",
"container": {
"node": "16"
}
}

17
server/api/hello.js Normal file
View File

@ -0,0 +1,17 @@
export default defineEventHandler(() => {
return {
title: 'Mount Everest',
description: "Mount Everest is Earth's highest mountain above sea level, located in the Mahalangur Himal sub-range of the Himalayas. The ChinaNepal border runs across its summit point",
height: '8,848 m',
countries: [
'China',
'Nepal'
],
continent: 'Asia',
image: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Everest_kalapatthar.jpg/600px-Everest_kalapatthar.jpg',
dir: '/mountains',
path: '/mountains/mount-everest',
slug: 'mount-everest',
updatedAt: '2020-12-11T15:40:35.000Z'
}
})

25
stores/counter.ts Normal file
View File

@ -0,0 +1,25 @@
import { defineStore } from 'pinia'
export interface ICounterState {
count: number
}
export const useCounter = defineStore('counter', {
state: (): ICounterState => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
reset() {
this.count = 0
},
increment2x() {
this.count *= 2
},
},
})

30
stores/identity.ts Normal file
View File

@ -0,0 +1,30 @@
import { defineStore } from 'pinia'
export interface IIdentityState {
firstName: string
lastName: string
}
export const useIdentity = defineStore('identity', {
state: (): IIdentityState => ({
firstName: 'Alfian',
lastName: 'Dwi',
}),
actions: {
setFirstName(firstName: string) {
this.firstName = firstName
},
setLastName(lastName: string) {
this.lastName = lastName
},
reset() {
this.firstName = 'Alfian'
this.lastName = 'Dwi'
},
},
getters: {
fullName(): string {
return `${this.firstName} ${this.lastName}`
},
},
})

20
tests/example.test.ts Normal file
View File

@ -0,0 +1,20 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { setup, $fetch, isDev } from '@nuxt/test-utils-edge'
describe('example', async () => {
await setup({
rootDir: fileURLToPath(new URL('..', import.meta.url)),
server: true,
})
it('Renders Nuxt 3 boilarplate', async () => {
expect(await $fetch('/')).toMatch('Nuxt 3 boilarplate')
})
if (isDev()) {
it('[dev] ensure vite client script is added', async () => {
expect(await $fetch('/')).toMatch('/_nuxt/@vite/client"')
})
}
})

Some files were not shown because too many files have changed in this diff Show More