package list from viandwi24
commit
4125135289
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
components.d.ts
|
||||
nuxt.d.ts
|
||||
dist
|
||||
.nuxt
|
||||
.output
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
*.log
|
||||
nuxt.d.ts
|
||||
|
||||
# private config
|
||||
.env
|
||||
|
||||
# deps
|
||||
node_modules
|
||||
|
||||
# build or generate
|
||||
.nuxt
|
||||
dist
|
||||
.output
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vercel
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit ""
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm install
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm lint-staged --no-stash
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"installDependencies": true,
|
||||
"startCommand": "npm run dev"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"Nuxt.mdc"
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"editor.tabSize": 2
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1,7 @@
|
|||
export default defineAppConfig({
|
||||
name: 'Nuxt 3 boilarplate',
|
||||
author: {
|
||||
name: '',
|
||||
link: '',
|
||||
},
|
||||
})
|
|
@ -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>
|
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 |
|
@ -0,0 +1 @@
|
|||
$padding: 0.05em;
|
|
@ -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;
|
||||
}
|
|
@ -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,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',
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
|
@ -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']
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<div class="fixed bg-black opacity-70 z-50 top-0 left-0 w-screen h-screen" />
|
||||
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<div class="card-content px-6 py-6 relative">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<LayoutPage>
|
||||
<Error :code="404" wrap />
|
||||
</LayoutPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LayoutPage from '~/layouts/page.vue'
|
||||
</script>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<section class="lg:px-8 px-4 mb-6">
|
||||
<slot />
|
||||
</section>
|
||||
</template>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<div class="flex-1 relative py-8">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
# Global Components
|
||||
|
||||
This directory make your components available globally.
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useLang = () => {
|
||||
const { t } = useI18n()
|
||||
return {
|
||||
t,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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/
|
||||
```
|
||||
:::
|
||||
::
|
||||
:::
|
|
@ -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')
|
||||
);
|
||||
```
|
||||
:::
|
||||
::
|
||||
:::
|
|
@ -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.
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
title: 'CONTENT_TITLE'
|
||||
description: 'CONTENT_DESCRIPTION'
|
||||
date: '2022-06-29'
|
||||
author: 'dwd'
|
||||
---
|
||||
|
||||
CONTENT_BODY
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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: 無効
|
|
@ -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: 장애가 있는
|
|
@ -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
|
|
@ -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: 禁用
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
})
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
})
|
||||
})
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 566 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"infiniteLoopProtection": true,
|
||||
"hardReloadOnChange": false,
|
||||
"view": "browser",
|
||||
"template": "nuxt",
|
||||
"container": {
|
||||
"node": "16"
|
||||
}
|
||||
}
|
|
@ -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 China–Nepal 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'
|
||||
}
|
||||
})
|
|
@ -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
|
||||
},
|
||||
},
|
||||
})
|
|
@ -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}`
|
||||
},
|
||||
},
|
||||
})
|
|
@ -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
Loading…
Reference in New Issue