mirror of
https://github.com/andatoshiki/toshiki-home-nuxt3.git
synced 2026-06-05 22:46:32 +00:00
init: initial commit for all project root sources
This commit is contained in:
commit
324edea85e
13
.editorconfig
Executable file
13
.editorconfig
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
7
.env.example
Executable file
7
.env.example
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
FIREBASE_API_KEY=""
|
||||||
|
FIREBASE_PROJECT_ID=""
|
||||||
|
FIREBASE_APP_ID=""
|
||||||
|
|
||||||
|
GOOGLE_ANALYTICS_ID=""
|
||||||
|
LASTFM_API_KEY=""
|
||||||
|
UMAMI_WEBSITE_ID=""
|
||||||
98
.gitignore
vendored
Executable file
98
.gitignore
vendored
Executable file
@ -0,0 +1,98 @@
|
|||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Node template
|
||||||
|
# Logs
|
||||||
|
/logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# Nuxt generate
|
||||||
|
dist
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless
|
||||||
|
|
||||||
|
# IDE / Editor
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Auto generated components file for WebStorm
|
||||||
|
.components.gen.js
|
||||||
|
|
||||||
|
# Service worker
|
||||||
|
sw.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Vim swap files
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Others
|
||||||
|
.yarn
|
||||||
|
.netlify
|
||||||
|
.pnpm
|
||||||
1
.node-version
Executable file
1
.node-version
Executable file
@ -0,0 +1 @@
|
|||||||
|
16
|
||||||
15
.prettierignore
Executable file
15
.prettierignore
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
# cache
|
||||||
|
cache
|
||||||
|
|
||||||
|
# built dist
|
||||||
|
dist
|
||||||
|
|
||||||
|
# scripts folder
|
||||||
|
scripts
|
||||||
|
|
||||||
|
# lock file
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# package file
|
||||||
|
package.json
|
||||||
9
.vscode/extensions.json
vendored
Executable file
9
.vscode/extensions.json
vendored
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"johnsoncodehk.volar",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"voorjaar.windicss-intellisense",
|
||||||
|
"editorconfig.editorconfig"
|
||||||
|
]
|
||||||
|
}
|
||||||
20
@types/runtimeConfig.d.ts
vendored
Executable file
20
@types/runtimeConfig.d.ts
vendored
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
/* Interfaces */
|
||||||
|
export interface SponsorLinks {
|
||||||
|
github: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Social {
|
||||||
|
discord: string
|
||||||
|
twitter: string
|
||||||
|
github: string
|
||||||
|
instagram: string
|
||||||
|
trello: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@nuxt/types/config/runtime' {
|
||||||
|
interface NuxtRuntimeConfig {
|
||||||
|
social: Social
|
||||||
|
sponsor: SponsorLinks
|
||||||
|
}
|
||||||
|
}
|
||||||
4
@types/vue.shim.d.ts
vendored
Executable file
4
@types/vue.shim.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import Vue from 'vue'
|
||||||
|
export default Vue
|
||||||
|
}
|
||||||
21
LICENSE
Executable file
21
LICENSE
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023-current Anda Toshiki <hello@toshiki.dev>
|
||||||
|
|
||||||
|
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.
|
||||||
24
config/buildModules.ts
Executable file
24
config/buildModules.ts
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
import { NuxtOptionsModule } from '@nuxt/types/config/module'
|
||||||
|
|
||||||
|
// Import module options
|
||||||
|
import colorMode from './modules/colorMode'
|
||||||
|
import windicss from './modules/windicss'
|
||||||
|
import image from './modules/image'
|
||||||
|
import googleAnalytics from './modules/googleAnalytics'
|
||||||
|
import typescriptBuild from './modules/typescriptBuild'
|
||||||
|
|
||||||
|
// Dev mode
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
const BuildModules: NuxtOptionsModule[] = [
|
||||||
|
'@nuxtjs/moment',
|
||||||
|
['@nuxt/image', image],
|
||||||
|
['nuxt-windicss', windicss],
|
||||||
|
['@nuxtjs/color-mode', colorMode],
|
||||||
|
['@nuxt/typescript-build', typescriptBuild],
|
||||||
|
['@nuxtjs/google-analytics', googleAnalytics]
|
||||||
|
]
|
||||||
|
|
||||||
|
if (isDev) BuildModules.unshift('nuxt-vite')
|
||||||
|
|
||||||
|
export default BuildModules
|
||||||
1
config/components.ts
Executable file
1
config/components.ts
Executable file
@ -0,0 +1 @@
|
|||||||
|
export default ['~/components']
|
||||||
14
config/constants.ts
Executable file
14
config/constants.ts
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
export default {
|
||||||
|
social: {
|
||||||
|
twitter: 'https://twitter.com/andatoshiki/',
|
||||||
|
github: 'https://github.com/andatoshiki/',
|
||||||
|
telegram: 'https://andatoshiki.t.me',
|
||||||
|
instagram: 'https://instagram.com/andatoshiki/',
|
||||||
|
youtube: 'https://youtube.com/@andatoshiki',
|
||||||
|
mastodon: 'https://mastodon.social/@andatoshiki',
|
||||||
|
email: 'hello@toshiki.dev'
|
||||||
|
},
|
||||||
|
sponsor: {
|
||||||
|
github: 'https://github.com/sponsors/andatoshiki'
|
||||||
|
}
|
||||||
|
}
|
||||||
1
config/css.ts
Executable file
1
config/css.ts
Executable file
@ -0,0 +1 @@
|
|||||||
|
export default ['@/stylesheets/root']
|
||||||
7
config/generate.ts
Executable file
7
config/generate.ts
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
import { NuxtOptionsGenerate } from '@nuxt/types/config/generate'
|
||||||
|
|
||||||
|
const Generate: NuxtOptionsGenerate = {
|
||||||
|
fallback: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Generate
|
||||||
106
config/head.ts
Executable file
106
config/head.ts
Executable file
@ -0,0 +1,106 @@
|
|||||||
|
import { NuxtOptionsHead } from '@nuxt/types/config/head'
|
||||||
|
|
||||||
|
/* Define constants */
|
||||||
|
const image = 'https://toshiki.dev/icon.png'
|
||||||
|
const description =
|
||||||
|
'Young JavaScript developer from Arizona, interested in languages, gaming, and programming, trying to improve his JavaScript skills!'
|
||||||
|
|
||||||
|
const Head: NuxtOptionsHead = {
|
||||||
|
title: 'toshiki.dev',
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' },
|
||||||
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
{
|
||||||
|
hid: 'description',
|
||||||
|
name: 'description',
|
||||||
|
content: description
|
||||||
|
},
|
||||||
|
/* Twitter */
|
||||||
|
{
|
||||||
|
hid: 'twitter:card',
|
||||||
|
name: 'twitter:card',
|
||||||
|
content: 'summary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hid: 'twitter:site',
|
||||||
|
name: 'twitter:site',
|
||||||
|
content: '@andatoshiki'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hid: 'twitter:creator',
|
||||||
|
name: 'twitter:creator',
|
||||||
|
content: '@andatoshiki'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hid: 'twitter:title',
|
||||||
|
name: 'twitter:title',
|
||||||
|
content: 'andatoshiki'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hid: 'twitter:description',
|
||||||
|
name: 'twitter:description',
|
||||||
|
content: description
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hid: 'twitter:image',
|
||||||
|
name: 'twitter:image',
|
||||||
|
content: image
|
||||||
|
},
|
||||||
|
/* Open-Graph */
|
||||||
|
{
|
||||||
|
hid: 'og:type',
|
||||||
|
name: 'og:type',
|
||||||
|
content: 'website'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hid: 'og:site_name',
|
||||||
|
name: 'og:site_name',
|
||||||
|
content: 'toshiki.dev'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hid: 'og:description',
|
||||||
|
name: 'og:description',
|
||||||
|
content: description
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hid: 'og:image',
|
||||||
|
name: 'og:image',
|
||||||
|
content: image
|
||||||
|
},
|
||||||
|
/* Others */
|
||||||
|
{
|
||||||
|
hid: 'theme-color',
|
||||||
|
name: 'theme-color',
|
||||||
|
content: '#171717'
|
||||||
|
}
|
||||||
|
].map(i => {
|
||||||
|
// @ts-ignore-next-line
|
||||||
|
if (i.name && !i.property) i.property = i.name
|
||||||
|
return i
|
||||||
|
}),
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/x-icon',
|
||||||
|
href: 'https://toshiki.dev/assets/icons/favicon.ico'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'search',
|
||||||
|
type: 'application/opensearchdescription+xml',
|
||||||
|
title: "'s Blog",
|
||||||
|
href: 'https://toshiki.dev/opensearch.xml'
|
||||||
|
},
|
||||||
|
{ rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css' }
|
||||||
|
],
|
||||||
|
|
||||||
|
script: [
|
||||||
|
{
|
||||||
|
async: true,
|
||||||
|
defer: true,
|
||||||
|
'data-website-id': `${process.env.UMAMI_WEBSITE_ID || ''}`,
|
||||||
|
src: `${process.env.UMAMI_ENDPOINT || ''}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Head
|
||||||
7
config/loading.ts
Executable file
7
config/loading.ts
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
import { NuxtOptionsLoading } from '@nuxt/types/config/loading'
|
||||||
|
|
||||||
|
const Loading: NuxtOptionsLoading = {
|
||||||
|
color: '#f3f4f6'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Loading
|
||||||
22
config/modules.ts
Executable file
22
config/modules.ts
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
import { NuxtOptionsModule } from '@nuxt/types/config/module'
|
||||||
|
|
||||||
|
/* Import module options */
|
||||||
|
import content from './modules/content'
|
||||||
|
import feed from './modules/feed'
|
||||||
|
import firebase from './modules/firebase'
|
||||||
|
import pwa from './modules/pwa'
|
||||||
|
import sitemap from './modules/sitemap'
|
||||||
|
import webfontloader from './modules/webfontloader'
|
||||||
|
|
||||||
|
const Modules: NuxtOptionsModule[] = [
|
||||||
|
'@nuxtjs/axios',
|
||||||
|
'@nuxtjs/robots',
|
||||||
|
['@nuxtjs/pwa', pwa],
|
||||||
|
['@nuxt/content', content],
|
||||||
|
['@nuxtjs/feed', feed],
|
||||||
|
['@nuxtjs/sitemap', sitemap],
|
||||||
|
['@nuxtjs/firebase', firebase],
|
||||||
|
['nuxt-webfontloader', webfontloader]
|
||||||
|
]
|
||||||
|
|
||||||
|
export default Modules
|
||||||
10
config/modules/colorMode.ts
Executable file
10
config/modules/colorMode.ts
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
import { ColorModeConfig } from '@nuxtjs/color-mode/types/color-mode'
|
||||||
|
|
||||||
|
const ColorMode: ColorModeConfig = {
|
||||||
|
storageKey: 'color-mode',
|
||||||
|
preference: 'system',
|
||||||
|
fallback: 'dark',
|
||||||
|
classSuffix: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ColorMode
|
||||||
32
config/modules/content.ts
Executable file
32
config/modules/content.ts
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
import type { IContentOptions } from '@nuxt/content/types/index'
|
||||||
|
|
||||||
|
// prism highlighter
|
||||||
|
const Content: IContentOptions = {
|
||||||
|
liveEdit: false,
|
||||||
|
dir: 'content',
|
||||||
|
markdown: {
|
||||||
|
prism: {
|
||||||
|
theme: 'prism-themes/themes/prism-one-dark.css'
|
||||||
|
},
|
||||||
|
/* @ts-ignore-next-line */
|
||||||
|
remarkExternalLinks: {
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noreferrer noopener'
|
||||||
|
},
|
||||||
|
remarkPlugins: [
|
||||||
|
[
|
||||||
|
'remark-autolink-headings',
|
||||||
|
{
|
||||||
|
behavior: 'append'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'remark-math'
|
||||||
|
],
|
||||||
|
rehypePlugins: [
|
||||||
|
// this next line here
|
||||||
|
['rehype-katex', { output: 'html' }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Content
|
||||||
48
config/modules/feed.ts
Executable file
48
config/modules/feed.ts
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
const Feed = () => {
|
||||||
|
const baseUrlArticles = 'https://toshiki.dev/blog'
|
||||||
|
|
||||||
|
const feedFormats = {
|
||||||
|
rss: { type: 'rss2', file: 'rss.xml' },
|
||||||
|
json: { type: 'json1', file: 'feed.json' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const { $content } = require('@nuxt/content')
|
||||||
|
|
||||||
|
const createFeedArticles = async function (feed: any) {
|
||||||
|
feed.options = {
|
||||||
|
title: "Toshiki's Blog",
|
||||||
|
description: 'Real life stories anecdotes & developmental journeys for embarking your inspirations.',
|
||||||
|
link: baseUrlArticles
|
||||||
|
}
|
||||||
|
|
||||||
|
const articles = await $content('blog').fetch()
|
||||||
|
|
||||||
|
articles.forEach((article: any) => {
|
||||||
|
const url = `${baseUrlArticles}/${article.slug}`
|
||||||
|
|
||||||
|
const hostName = process.env.NODE_ENV === 'production' ? 'https://toshiki.dev' : 'http://localhost:3000'
|
||||||
|
|
||||||
|
const postImagesPath = `${hostName}/assets/images/posts`
|
||||||
|
|
||||||
|
feed.addItem({
|
||||||
|
title: article.title,
|
||||||
|
slug: article.slug,
|
||||||
|
link: url,
|
||||||
|
image: article.image
|
||||||
|
? `${hostName}${article.image}`
|
||||||
|
: `${postImagesPath}/${url?.split('/')?.at(-1)}.jpg`,
|
||||||
|
date: new Date(article.createdAt),
|
||||||
|
description: article.description,
|
||||||
|
content: article.summary
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(feedFormats).map(({ file, type }) => ({
|
||||||
|
path: `${file}`,
|
||||||
|
create: createFeedArticles,
|
||||||
|
type
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Feed
|
||||||
31
config/modules/firebase.ts
Executable file
31
config/modules/firebase.ts
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
import { FirebaseModuleConfiguration } from '@nuxtjs/firebase/types/index'
|
||||||
|
import { config as loadEnv } from 'dotenv'
|
||||||
|
|
||||||
|
/* Load env variables */
|
||||||
|
loadEnv()
|
||||||
|
|
||||||
|
const { FIREBASE_APP_ID: appId, FIREBASE_API_KEY: apiKey, FIREBASE_PROJECT_ID: projectId } = process.env
|
||||||
|
|
||||||
|
if (!appId || !apiKey || !projectId)
|
||||||
|
throw new Error(
|
||||||
|
'You are missing Firebase options, please check your .env file and make sure all required keys are present.'
|
||||||
|
)
|
||||||
|
|
||||||
|
const Firebase: FirebaseModuleConfiguration = {
|
||||||
|
config: {
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
|
projectId,
|
||||||
|
// Setting these because types aren't optional, though they're not required
|
||||||
|
databaseURL: 'https://toshiki-home-nuxt-default-rtdb.asia-southeast1.firebasedatabase.app',
|
||||||
|
authDomain: 'toshiki-home-nuxt.firebase.app',
|
||||||
|
storageBucket: 'toshiki-home-nuxt.appspot.com',
|
||||||
|
messagingSenderId: '765131615342',
|
||||||
|
measurementId: 'G-GWD4JF3M6Z'
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
firestore: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Firebase
|
||||||
7
config/modules/googleAnalytics.ts
Executable file
7
config/modules/googleAnalytics.ts
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
import { InstallOptions } from 'vue-analytics'
|
||||||
|
|
||||||
|
const GoogleAnalytics: InstallOptions = {
|
||||||
|
id: process.env.GOOGLE_ANALYTICS_ID || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GoogleAnalytics
|
||||||
14
config/modules/image.ts
Executable file
14
config/modules/image.ts
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
const NuxtImage = {
|
||||||
|
domains: [
|
||||||
|
'https://http.toshiki.dev',
|
||||||
|
'https://lastfm.freetls.fastly.net',
|
||||||
|
'https://cdn.jsdelivr.net',
|
||||||
|
'https://avatars.githubusercontent.com',
|
||||||
|
'https://proxy.duckduckgo.com',
|
||||||
|
'https://r2.toshiki.dev',
|
||||||
|
'https://bucket.toshiki.dev',
|
||||||
|
'https://jsd.toshiki.dev'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NuxtImage
|
||||||
8
config/modules/pwa.ts
Executable file
8
config/modules/pwa.ts
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
manifest: {
|
||||||
|
name: 'toshiki.dev',
|
||||||
|
short_name: 'toshiki.dev',
|
||||||
|
theme_color: '#f56565',
|
||||||
|
lang: 'en'
|
||||||
|
}
|
||||||
|
}
|
||||||
15
config/modules/sitemap.ts
Executable file
15
config/modules/sitemap.ts
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
export default async function () {
|
||||||
|
const { $content } = require('@nuxt/content')
|
||||||
|
const posts = await $content('blog').fetch()
|
||||||
|
|
||||||
|
const routes = []
|
||||||
|
for (const post of posts) {
|
||||||
|
routes.push(`blog/${post.slug}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostname: 'https://toshiki.dev',
|
||||||
|
gzip: true,
|
||||||
|
routes
|
||||||
|
}
|
||||||
|
}
|
||||||
8
config/modules/typescriptBuild.ts
Executable file
8
config/modules/typescriptBuild.ts
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
import { Options } from '@nuxt/typescript-build'
|
||||||
|
|
||||||
|
const TypescriptBuild: Options = {
|
||||||
|
// Disabling type checking since we have it on our editor and don't want it to slow down the build process
|
||||||
|
typeCheck: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TypescriptBuild
|
||||||
5
config/modules/vite.ts
Executable file
5
config/modules/vite.ts
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
experimentWarning: false,
|
||||||
|
build: false,
|
||||||
|
ssr: false
|
||||||
|
}
|
||||||
6
config/modules/webfontloader.ts
Executable file
6
config/modules/webfontloader.ts
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
custom: {
|
||||||
|
families: ['Inter'],
|
||||||
|
urls: ['https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap']
|
||||||
|
}
|
||||||
|
}
|
||||||
8
config/modules/windicss.ts
Executable file
8
config/modules/windicss.ts
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import { ModuleOptions } from 'nuxt-windicss/dist/types'
|
||||||
|
|
||||||
|
const windicss: ModuleOptions = {
|
||||||
|
config: resolve('windi.config.ts')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default windicss
|
||||||
30
config/plugins.ts
Executable file
30
config/plugins.ts
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
import { NuxtOptionsPlugin } from '@nuxt/types/config/plugin'
|
||||||
|
|
||||||
|
const Plugins: NuxtOptionsPlugin[] = [
|
||||||
|
'@/plugins/Util',
|
||||||
|
'@/plugins/Types',
|
||||||
|
'@/plugins/Disqus',
|
||||||
|
{
|
||||||
|
src: '@/plugins/CmdK',
|
||||||
|
mode: 'client'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '@/plugins/Lanyard',
|
||||||
|
mode: 'client'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '@/plugins/Firebase',
|
||||||
|
mode: 'client'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '@/plugins/Tippy',
|
||||||
|
mode: 'client'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '@/plugins/DPlayer',
|
||||||
|
mode: 'client',
|
||||||
|
ssr: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default Plugins
|
||||||
7
config/publicRuntimeConfig.ts
Executable file
7
config/publicRuntimeConfig.ts
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
/* Import constants */
|
||||||
|
import constants from './constants'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...constants,
|
||||||
|
isDev: process.env.NODE_ENV === 'development'
|
||||||
|
}
|
||||||
47
hooks/generate/done.ts
Executable file
47
hooks/generate/done.ts
Executable file
@ -0,0 +1,47 @@
|
|||||||
|
import { existsSync, writeFileSync, mkdirSync } from 'fs'
|
||||||
|
import consola from 'consola'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
// Scripts
|
||||||
|
import { generateImage } from '../../scripts/generateOgImage'
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
import getReadingTime from '../../src/plugins/Utils/getReadingTime'
|
||||||
|
|
||||||
|
// english i18n
|
||||||
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
|
||||||
|
export const generateDone = async (generator: any) => {
|
||||||
|
const generateDir = generator.nuxt.options.generate.dir
|
||||||
|
const folderPath = join(generateDir, './og-images/')
|
||||||
|
|
||||||
|
const { $content } = require('@nuxt/content')
|
||||||
|
const articles = await $content('blog').fetch()
|
||||||
|
|
||||||
|
if (!articles.length) return
|
||||||
|
|
||||||
|
consola.info(`Generating OG images for ${articles.length} posts.`)
|
||||||
|
|
||||||
|
for (const article of articles) {
|
||||||
|
const { title, description, slug, body, createdAt, tags } = article
|
||||||
|
|
||||||
|
const readingTime = getReadingTime(JSON.stringify(body))
|
||||||
|
const postDate = formatter.format(new Date(createdAt)).split('.').join('/')
|
||||||
|
|
||||||
|
const metaImage = await generateImage({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
subtitles: [postDate, `${readingTime} Minutes`, `#${tags[0]}`]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!existsSync(folderPath)) mkdirSync(folderPath)
|
||||||
|
|
||||||
|
writeFileSync(join(folderPath, `./${slug}.png`), metaImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
consola.success(`Generated ${articles.length} OG images.`)
|
||||||
|
}
|
||||||
13
jsconfig.json
Executable file
13
jsconfig.json
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./*"],
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"~~/*": ["./*"],
|
||||||
|
"@@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", ".nuxt", "dist"]
|
||||||
|
}
|
||||||
5
netlify.toml
Executable file
5
netlify.toml
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
[[redirects]]
|
||||||
|
force = true
|
||||||
|
from = "/blog/gonderi/:slug"
|
||||||
|
status = 301
|
||||||
|
to = "/blog/:slug"
|
||||||
94
netlify/functions/getLastFmSongs.ts
Executable file
94
netlify/functions/getLastFmSongs.ts
Executable file
@ -0,0 +1,94 @@
|
|||||||
|
import { Handler } from '@netlify/functions'
|
||||||
|
import LastFMTyped from 'lastfm-typed'
|
||||||
|
|
||||||
|
// Can also be set through Netlify environment variables
|
||||||
|
const LASTFM_API_KEY = process.env.LASTFM_API_KEY
|
||||||
|
const username = 'andatoshiki'
|
||||||
|
|
||||||
|
const handler: Handler = async () => {
|
||||||
|
if (!LASTFM_API_KEY)
|
||||||
|
return {
|
||||||
|
statusCode: 401
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lastFm = new LastFMTyped(LASTFM_API_KEY)
|
||||||
|
|
||||||
|
const [info, topTracks, topArtists, recentTracks] = [
|
||||||
|
await lastFm.user.getInfo(username),
|
||||||
|
await lastFm.user.getTopTracks(username, { limit: 6, period: '7day' }),
|
||||||
|
await lastFm.user.getTopArtists(username, { limit: 4, period: '7day' }),
|
||||||
|
await lastFm.user.getRecentTracks(username, { limit: 15 })
|
||||||
|
]
|
||||||
|
|
||||||
|
// Origin for CORS
|
||||||
|
const origin = process.env.NODE_ENV === 'production' ? 'https://toshiki.dev' : 'http://localhost:*'
|
||||||
|
|
||||||
|
// Map track function
|
||||||
|
const mapTrack = (track: any): any => {
|
||||||
|
const artist = typeof track.artist === 'string' ? track.artist : track.artist.name
|
||||||
|
|
||||||
|
const object: any = {
|
||||||
|
artist,
|
||||||
|
name: track.name,
|
||||||
|
image: track.image.find((image: any) => image.size === 'large')?.url,
|
||||||
|
url: track.url,
|
||||||
|
date: track.date?.uts,
|
||||||
|
nowPlaying: track.nowplaying
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track.playcount) object.plays = track.playcount
|
||||||
|
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map artist function
|
||||||
|
const mapArtist = (artist: any) => {
|
||||||
|
const object: any = {
|
||||||
|
name: artist.name,
|
||||||
|
image: artist.image.find((image: any) => image.size === 'large')?.url,
|
||||||
|
url: artist.url
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artist.playcount) object.plays = artist.playcount
|
||||||
|
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatted user info
|
||||||
|
const formattedUserInfo = {
|
||||||
|
name: info.name,
|
||||||
|
image: info.image.find(image => image.size === 'large')?.url,
|
||||||
|
url: info.url,
|
||||||
|
totalPlays: info.playcount,
|
||||||
|
registered: info.registered
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: false,
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': origin,
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type',
|
||||||
|
'Access-Control-Allow-Methods': 'GET'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
user: formattedUserInfo,
|
||||||
|
recentTracks: recentTracks?.tracks?.map(mapTrack) || [],
|
||||||
|
topTracks: topTracks?.tracks?.map(mapTrack) || [],
|
||||||
|
topArtists: topArtists?.artists?.map(mapArtist) || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error)
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
statusCode: error.statusCode || 500,
|
||||||
|
message: error.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { handler }
|
||||||
63
nuxt.config.ts
Executable file
63
nuxt.config.ts
Executable file
@ -0,0 +1,63 @@
|
|||||||
|
// Types
|
||||||
|
import type { NuxtConfig } from '@nuxt/types'
|
||||||
|
|
||||||
|
// Base config
|
||||||
|
import buildModules from './config/buildModules'
|
||||||
|
import components from './config/components'
|
||||||
|
import generate from './config/generate'
|
||||||
|
import css from './config/css'
|
||||||
|
import head from './config/head'
|
||||||
|
import loading from './config/loading'
|
||||||
|
import modules from './config/modules'
|
||||||
|
import plugins from './config/plugins'
|
||||||
|
import publicRuntimeConfig from './config/publicRuntimeConfig'
|
||||||
|
|
||||||
|
// Specific module options
|
||||||
|
import vite from './config/modules/vite'
|
||||||
|
import feed from './config/modules/feed'
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
import { generateDone } from './hooks/generate/done'
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
const Config: NuxtConfig = {
|
||||||
|
// Constant options
|
||||||
|
rootDir: './',
|
||||||
|
srcDir: 'src',
|
||||||
|
target: 'static',
|
||||||
|
|
||||||
|
/*
|
||||||
|
Disabling server-side rendering on development mode because
|
||||||
|
Vite module currently doesn't work when SSR is enabled. This
|
||||||
|
might cause some issues and/or hydration errors but will be
|
||||||
|
effective enough to help you develop easier.
|
||||||
|
*/
|
||||||
|
ssr: !isDev,
|
||||||
|
|
||||||
|
// Imported options
|
||||||
|
head,
|
||||||
|
loading,
|
||||||
|
buildModules,
|
||||||
|
components,
|
||||||
|
generate,
|
||||||
|
css,
|
||||||
|
modules,
|
||||||
|
plugins,
|
||||||
|
publicRuntimeConfig,
|
||||||
|
|
||||||
|
hooks: {
|
||||||
|
generate: {
|
||||||
|
async done(generator) {
|
||||||
|
await generateDone(generator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
vite,
|
||||||
|
feed
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Config
|
||||||
94
package.json
Executable file
94
package.json
Executable file
@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"version": "6.0.0",
|
||||||
|
"homepage": "https://toshiki.dev",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.9"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "Anda Toshiki",
|
||||||
|
"email": "hello@toshiki.dev",
|
||||||
|
"url": "https://toshiki.dev"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"email": "hello@toshiki.dev",
|
||||||
|
"url": "https://github.com/andatoshiki/toshiki-home-nuxt3/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/andatoshiki/toshiki-home-nuxt3.git"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://toshiki.dev/donate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/andatoshiki"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nuxt",
|
||||||
|
"build": "nuxt build",
|
||||||
|
"start": "nuxt start",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"lint": "prettier --write .",
|
||||||
|
"function": "netlify functions:serve"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@andatoshiki/vue-lanyard": "^0.0.0",
|
||||||
|
"@netlify/functions": "^1.4.0",
|
||||||
|
"@nuxt/content": "1.15.1",
|
||||||
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
"@nuxtjs/feed": "^2.0.0",
|
||||||
|
"@nuxtjs/pwa": "^3.3.5",
|
||||||
|
"@nuxtjs/robots": "^2.5.0",
|
||||||
|
"@nuxtjs/sitemap": "^2.4.0",
|
||||||
|
"@types/prismjs": "^1.26.0",
|
||||||
|
"@vue/runtime-dom": "^3.2.47",
|
||||||
|
"core-js": "^3.30.1",
|
||||||
|
"lastfm-typed": "^2.1.0",
|
||||||
|
"medium-zoom": "^1.0.8",
|
||||||
|
"moment-timezone": "^0.5.43",
|
||||||
|
"nuxt-vite": "^0.3.5",
|
||||||
|
"nuxt-webfontloader": "^1.1.0",
|
||||||
|
"prism-themes": "^1.9.0",
|
||||||
|
"prismjs": "^1.29.0",
|
||||||
|
"sass": "^1.62.0",
|
||||||
|
"vue-cmd-menu": "^0.1.9",
|
||||||
|
"vue-disqus": "^4.0.1",
|
||||||
|
"vue-tippy": "^4.16.0",
|
||||||
|
"webpack": "^4.46.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.21.4",
|
||||||
|
"@nuxt/image": "^0.7.1",
|
||||||
|
"@nuxt/types": "^2.16.3",
|
||||||
|
"@nuxt/typescript-build": "^3.0.1",
|
||||||
|
"@nuxtjs/color-mode": "2.1.1",
|
||||||
|
"@nuxtjs/firebase": "^8.2.2",
|
||||||
|
"@nuxtjs/google-analytics": "^2.4.0",
|
||||||
|
"@nuxtjs/moment": "^1.6.1",
|
||||||
|
"@types/react": "^18.0.37",
|
||||||
|
"@types/sharp": "^0.31.1",
|
||||||
|
"client": "link:@@waline/client",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"dplayer": "^1.27.1",
|
||||||
|
"firebase": "^10.11.0",
|
||||||
|
"isomorphic-unfetch": "^4.0.2",
|
||||||
|
"netlify-cli": "^17.22.1",
|
||||||
|
"nuxt": "^2.17.0",
|
||||||
|
"nuxt-windicss": "^2.6.1",
|
||||||
|
"postcss-preset-env": "^8.3.2",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"rehype-katex": "^5.0.0",
|
||||||
|
"remark-math": "^4.0.0",
|
||||||
|
"sass-loader": "10.1.1",
|
||||||
|
"satori": "^0.4.13",
|
||||||
|
"sharp": "^0.32.0",
|
||||||
|
"vite": "^4.2.2",
|
||||||
|
"vue-dplayer": "^0.0.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
20510
pnpm-lock.yaml
Normal file
20510
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
48
prettier.config.js
Executable file
48
prettier.config.js
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
module.exports = {
|
||||||
|
tabWidth: 4,
|
||||||
|
printWidth: 120,
|
||||||
|
proseWrap: 'preserve',
|
||||||
|
semi: false,
|
||||||
|
trailingComma: 'none',
|
||||||
|
singleQuote: true,
|
||||||
|
arrowParens: 'avoid',
|
||||||
|
endOfLine: 'lf',
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: '{*.md,.prettierrc,.stylelintrc,.babelrc,.html}',
|
||||||
|
options: {
|
||||||
|
tabWidth: 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: '{*.js?(on),*.js, *.css, *.scss, *.vue}',
|
||||||
|
options: {
|
||||||
|
trailingComma: 'none',
|
||||||
|
tabWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: '{*.ts}',
|
||||||
|
options: {
|
||||||
|
trailingComma: 'none',
|
||||||
|
tabWidth: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: '{**/.vscode/*.json,**/tsconfig.json,**/tsconfig.*.json}',
|
||||||
|
options: {
|
||||||
|
parser: 'json5',
|
||||||
|
quoteProps: 'preserve',
|
||||||
|
singleQuote: false,
|
||||||
|
trailingComma: 'all',
|
||||||
|
tabWidth: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: '*.y?(a)ml,',
|
||||||
|
options: {
|
||||||
|
singleQuote: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
147
scripts/generateOgImage.ts
Executable file
147
scripts/generateOgImage.ts
Executable file
@ -0,0 +1,147 @@
|
|||||||
|
import satori from "satori"
|
||||||
|
import { readFileSync } from "fs"
|
||||||
|
import sharp from "sharp"
|
||||||
|
|
||||||
|
// Polyfill
|
||||||
|
import "isomorphic-unfetch"
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
const Inter = readFileSync("./src/assets/fonts/Inter-Regular.ttf")
|
||||||
|
const InterBold = readFileSync("./src/assets/fonts/Inter-Bold.ttf")
|
||||||
|
|
||||||
|
interface IMetaImage {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
subtitles?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojis = [
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f4ab.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f636.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f44f.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f973.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/2728.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/2709.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/2600.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f30d.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f4a5.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f525.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f929.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/26a1.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/231b.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/2b50.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f4eb.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f4a1.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f4ad.png",
|
||||||
|
"https://cdnjs.toshiki.dev/ajax/libs/twemoji/14.0.2/72x72/1f389.png",
|
||||||
|
]
|
||||||
|
|
||||||
|
export const generateImage = async ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
subtitles,
|
||||||
|
}: IMetaImage) => {
|
||||||
|
const svg = await satori(
|
||||||
|
{
|
||||||
|
type: "div",
|
||||||
|
key: 1,
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderRadius: 24,
|
||||||
|
backgroundImage:
|
||||||
|
"url(https://raw.toshiki.dev/andatoshiki/toshiki-home-nuxt/master/src/static/assets/meta/images/post-background.png)",
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
type: "div",
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
marginLeft: 40,
|
||||||
|
marginRight: 40,
|
||||||
|
marginBottom: 40,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "img",
|
||||||
|
props: {
|
||||||
|
src: emojis[Math.floor(Math.random() * emojis.length)],
|
||||||
|
height: 80,
|
||||||
|
width: 80,
|
||||||
|
style: {
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "h1",
|
||||||
|
props: {
|
||||||
|
style: { fontSize: 60, fontWeight: 600, marginBottom: 0 },
|
||||||
|
children: title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "p",
|
||||||
|
props: {
|
||||||
|
style: { fontSize: 35, color: "rgba(0, 0, 0, 0.5)" },
|
||||||
|
children: description,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "div",
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
display: "flex",
|
||||||
|
marginTop: 12,
|
||||||
|
fontSize: 25,
|
||||||
|
},
|
||||||
|
children:
|
||||||
|
subtitles?.map((item, index) => ({
|
||||||
|
type: "span",
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: "6px 16px",
|
||||||
|
marginLeft: index === 0 ? 0 : 14,
|
||||||
|
color: "rgba(0, 0, 0, 0.5)",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||||
|
},
|
||||||
|
children: item,
|
||||||
|
},
|
||||||
|
})) || [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 1200,
|
||||||
|
height: 675,
|
||||||
|
fonts: [
|
||||||
|
{
|
||||||
|
name: "Inter",
|
||||||
|
data: Buffer.from(Inter),
|
||||||
|
weight: 400,
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Inter",
|
||||||
|
data: Buffer.from(InterBold),
|
||||||
|
weight: 700,
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const png = await sharp(Buffer.from(svg)).png().toBuffer()
|
||||||
|
return png
|
||||||
|
}
|
||||||
68
src/assets/files/premid/largeImages.ts
Executable file
68
src/assets/files/premid/largeImages.ts
Executable file
@ -0,0 +1,68 @@
|
|||||||
|
const general = [
|
||||||
|
{ name: 'Extreme Thinking', url: 'https://i.imgur.com/IJbthB1.png' },
|
||||||
|
{ name: 'Angry Stickman', url: 'https://i.imgur.com/90lDk9T.png' },
|
||||||
|
{ name: 'Read The Docs', url: 'https://i.imgur.com/PvrqhRJ.png' },
|
||||||
|
{ name: 'Please Stop', url: 'https://i.imgur.com/qgCcDu7.png' },
|
||||||
|
{ name: 'Brilliance', url: 'https://i.imgur.com/skclu5h.png' },
|
||||||
|
{ name: 'Pepe Sweat', url: 'https://i.imgur.com/j4ip0Dx.png' },
|
||||||
|
{ name: 'Panda Cry', url: 'https://i.imgur.com/NFCmYsM.png' },
|
||||||
|
{ name: 'Thinking', url: 'https://i.imgur.com/mzlKBw3.png' },
|
||||||
|
{ name: 'Spongery', url: 'https://i.imgur.com/VfhmSfN.png' },
|
||||||
|
{ name: 'Balance', url: 'https://i.imgur.com/v0jG4vt.png' },
|
||||||
|
{ name: 'Bravery', url: 'https://i.imgur.com/etvIz6E.png' },
|
||||||
|
{ name: 'Playing', url: 'https://i.imgur.com/UHgwoyQ.png' },
|
||||||
|
{ name: 'Reading', url: 'https://i.imgur.com/tbSYfJV.png' },
|
||||||
|
{ name: 'Popcorn', url: 'https://i.imgur.com/dQ4EOWV.png' },
|
||||||
|
{ name: 'Windows', url: 'https://i.imgur.com/YkOU4HD.png' },
|
||||||
|
{ name: 'Mobile', url: 'https://i.imgur.com/BhBThRQ.png' },
|
||||||
|
{ name: 'Paused', url: 'https://i.imgur.com/TYvgF3M.png' },
|
||||||
|
{ name: 'Search', url: 'https://i.imgur.com/hnDIQO1.png' },
|
||||||
|
{ name: 'Please', url: 'https://i.imgur.com/Zp835mC.png' },
|
||||||
|
{ name: 'Sadcat', url: 'https://i.imgur.com/IaSeSJk.png' },
|
||||||
|
{ name: 'Coffee', url: 'https://i.imgur.com/W5NIvJF.png' },
|
||||||
|
{ name: 'Doubt', url: 'https://i.imgur.com/kYKE2rI.png' },
|
||||||
|
{ name: 'Relax', url: 'https://i.imgur.com/BaOQ4I8.png' },
|
||||||
|
{ name: 'Smart', url: 'https://i.imgur.com/vKhMs2R.png' },
|
||||||
|
{ name: 'Heart', url: 'https://i.imgur.com/jtt9fjf.png' },
|
||||||
|
{ name: 'Shrug', url: 'https://i.imgur.com/UnzU96q.png' },
|
||||||
|
{ name: 'Mmlol', url: 'https://i.imgur.com/5t2q2eu.png' },
|
||||||
|
{ name: 'Linux', url: 'https://i.imgur.com/bN5rmiU.png' },
|
||||||
|
{ name: 'Live', url: 'https://i.imgur.com/qphbAuR.png' },
|
||||||
|
{ name: 'Cool', url: 'https://i.imgur.com/AdUBBHa.png' },
|
||||||
|
{ name: 'Eyes', url: 'https://i.imgur.com/lIa90i4.png' },
|
||||||
|
{ name: 'Ohno', url: 'https://i.imgur.com/7EkHsMr.png' },
|
||||||
|
{ name: 'Tada', url: 'https://i.imgur.com/nO8fd9v.png' },
|
||||||
|
{ name: 'ESL', url: 'https://i.imgur.com/F51eSEx.png' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const brand = [
|
||||||
|
{ name: 'Facebook', url: 'https://i.imgur.com/5Aab3v6.png' },
|
||||||
|
{ name: 'Instagram', url: 'https://i.imgur.com/1c5yuiD.png' },
|
||||||
|
{ name: 'YouTube', url: 'https://i.imgur.com/0Bvl6BU.png' },
|
||||||
|
{ name: 'YouTube Dark', url: 'https://i.imgur.com/mQQO1nv.jpg' },
|
||||||
|
{ name: 'Netflix', url: 'https://i.imgur.com/DkZQvkC.png' },
|
||||||
|
{ name: 'Twitter', url: 'https://i.imgur.com/AtV70mE.png' },
|
||||||
|
{ name: 'Twitch', url: 'https://i.imgur.com/bmIsItf.png' },
|
||||||
|
{ name: 'Discord', url: 'https://i.imgur.com/P6fs8jR.png' },
|
||||||
|
{ name: 'Discord Templates', url: 'https://i.imgur.com/zqdRKw4.png' }
|
||||||
|
]
|
||||||
|
|
||||||
|
/* Exports */
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
name: 'General',
|
||||||
|
items: general
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Brand',
|
||||||
|
items: brand
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface Type {
|
||||||
|
name: string
|
||||||
|
items: {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
48
src/assets/files/premid/smallImages.ts
Executable file
48
src/assets/files/premid/smallImages.ts
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
const general = [
|
||||||
|
{ name: 'DoNotDisturb', url: 'https://i.imgur.com/MvrnrMW.png' },
|
||||||
|
{ name: 'Brilliance', url: 'https://i.imgur.com/skclu5h.png' },
|
||||||
|
{ name: 'VideoCall', url: 'https://i.imgur.com/GwMdYmz.png' },
|
||||||
|
{ name: 'Checkmark', url: 'https://i.imgur.com/FH7OH6y.png' },
|
||||||
|
{ name: 'Thinking', url: 'https://i.imgur.com/mzlKBw3.png' },
|
||||||
|
{ name: 'Windows', url: 'https://i.imgur.com/YkOU4HD.png' },
|
||||||
|
{ name: 'NoEntry', url: 'https://i.imgur.com/jVidfcx.png' },
|
||||||
|
{ name: 'Balance', url: 'https://i.imgur.com/v0jG4vt.png' },
|
||||||
|
{ name: 'Bravery', url: 'https://i.imgur.com/etvIz6E.png' },
|
||||||
|
{ name: 'Playing', url: 'https://i.imgur.com/UHgwoyQ.png' },
|
||||||
|
{ name: 'Writing', url: 'https://i.imgur.com/4VcqgYA.png' },
|
||||||
|
{ name: 'Reading', url: 'https://i.imgur.com/tbSYfJV.png' },
|
||||||
|
{ name: 'Coffee', url: 'https://i.imgur.com/W5NIvJF.png' },
|
||||||
|
{ name: 'Online', url: 'https://i.imgur.com/8cel80u.png' },
|
||||||
|
{ name: 'Please', url: 'https://i.imgur.com/d10KCzD.png' },
|
||||||
|
{ name: 'Paused', url: 'https://i.imgur.com/TYvgF3M.png' },
|
||||||
|
{ name: 'Search', url: 'https://i.imgur.com/hnDIQO1.png' },
|
||||||
|
{ name: 'Mmlol', url: 'https://i.imgur.com/5t2q2eu.png' },
|
||||||
|
{ name: 'Heart', url: 'https://i.imgur.com/jtt9fjf.png' },
|
||||||
|
{ name: 'Linux', url: 'https://i.imgur.com/bN5rmiU.png' },
|
||||||
|
{ name: 'Live', url: 'https://i.imgur.com/qphbAuR.png' },
|
||||||
|
{ name: 'Call', url: 'https://i.imgur.com/0akjqyz.png' },
|
||||||
|
{ name: 'Idle', url: 'https://i.imgur.com/mKIQ8Zo.png' },
|
||||||
|
{ name: 'Cool', url: 'https://i.imgur.com/AdUBBHa.png' },
|
||||||
|
{ name: 'Tada', url: 'https://i.imgur.com/nO8fd9v.png' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const brand = [
|
||||||
|
{ name: 'YouTube', url: 'https://i.imgur.com/0Bvl6BU.png' },
|
||||||
|
{ name: 'YouTube Dark', url: 'https://i.imgur.com/mQQO1nv.jpg' },
|
||||||
|
{ name: 'Netflix', url: 'https://i.imgur.com/DkZQvkC.png' },
|
||||||
|
{ name: 'Twitter', url: 'https://i.imgur.com/AtV70mE.png' },
|
||||||
|
{ name: 'Twitch', url: 'https://i.imgur.com/bmIsItf.png' },
|
||||||
|
{ name: 'Discord', url: 'https://i.imgur.com/P6fs8jR.png' }
|
||||||
|
]
|
||||||
|
|
||||||
|
/* Exports */
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
name: 'General',
|
||||||
|
items: general
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Brand',
|
||||||
|
items: brand
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
src/assets/fonts/Inter-Bold.ttf
Executable file
BIN
src/assets/fonts/Inter-Bold.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Inter-Regular.ttf
Executable file
BIN
src/assets/fonts/Inter-Regular.ttf
Executable file
Binary file not shown.
5
src/components/Blog/ImageContainer.vue
Executable file
5
src/components/Blog/ImageContainer.vue
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 md:(grid grid-flow-col auto-cols-fr)">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
61
src/components/Blog/Notification.vue
Executable file
61
src/components/Blog/Notification.vue
Executable file
@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'information'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getIcon() {
|
||||||
|
if (this.type === 'warning') return '❗️'
|
||||||
|
else if (this.type === 'danger') return '🚨'
|
||||||
|
else if (this.type === 'success') return '✅'
|
||||||
|
else return '💡'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="notification flex flex-col md:(items-center flex-row) gap-x-4 gap-y-2" :class="type">
|
||||||
|
<span class="text-xl md:text-lg">{{ getIcon }}</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 v-if="title">{{ title }}</h1>
|
||||||
|
|
||||||
|
<p v-if="!!$slots.default">
|
||||||
|
<slot />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.notification,
|
||||||
|
.nuxt-content .notification {
|
||||||
|
@apply rounded-lg border-[0.1px] my-5 p-4 bg-opacity-25 bg-neutral-300 border-neutral-200 dark:(bg-neutral-800/30 border-neutral-800);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
@apply font-medium text-lg m-0 hover:no-underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
p strong,
|
||||||
|
a {
|
||||||
|
@apply m-0 dark:text-white/70;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply font-medium text-current underline hover:underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
src/components/Blog/PrevNext.vue
Executable file
65
src/components/Blog/PrevNext.vue
Executable file
@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
/* Interfaces */
|
||||||
|
import type { FetchReturn } from '@nuxt/content/types/query-builder'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
currentSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
prev: {} as FetchReturn,
|
||||||
|
next: {} as FetchReturn
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetch() {
|
||||||
|
const [prev, next] = (await this.$content('blog')
|
||||||
|
.only(['title', 'slug'])
|
||||||
|
.sortBy('createdAt', 'asc')
|
||||||
|
.surround(this.currentSlug)
|
||||||
|
.fetch()) as FetchReturn[]
|
||||||
|
|
||||||
|
this.prev = prev
|
||||||
|
this.next = next
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<div
|
||||||
|
v-if="$fetchState.pending === false && !$fetchState.error"
|
||||||
|
class="grid gap-x-4 gap-y-2 grid-cols-1 md:grid-cols-2"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="prev ? 'SmartLink' : 'div'"
|
||||||
|
:href="prev && `/blog/${prev.slug}`"
|
||||||
|
class="rounded-lg card-base flex items-center space-x-2"
|
||||||
|
:class="!prev ? 'cursor-not-allowed' : 'dark:hover:text-white hover:bg-opacity-40'"
|
||||||
|
>
|
||||||
|
<IconChevron left class="h-4 w-4 flex-shrink-0" />
|
||||||
|
|
||||||
|
<span v-if="prev" class="truncate">{{ prev.title }}</span>
|
||||||
|
<span v-else class="truncate">No former post</span>
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<component
|
||||||
|
:is="next ? 'SmartLink' : 'div'"
|
||||||
|
:href="next && `/blog/${next.slug}`"
|
||||||
|
class="rounded-lg card-base flex items-center space-x-2 justify-end"
|
||||||
|
:class="!next ? 'cursor-not-allowed' : 'dark:hover:text-white hover:bg-opacity-40'"
|
||||||
|
>
|
||||||
|
<span v-if="next" class="truncate">{{ next.title }}</span>
|
||||||
|
<span v-else class="truncate">No latter post</span>
|
||||||
|
|
||||||
|
<IconChevron right class="h-4 w-4 flex-shrink-0" />
|
||||||
|
</component>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
128
src/components/Blog/Rating.vue
Executable file
128
src/components/Blog/Rating.vue
Executable file
@ -0,0 +1,128 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
/* Interfaces */
|
||||||
|
interface Platform {
|
||||||
|
platform?: string
|
||||||
|
classes?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Status {
|
||||||
|
component?: string
|
||||||
|
title?: string
|
||||||
|
classes?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
rating: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
default: '0'
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: false,
|
||||||
|
default: '10'
|
||||||
|
},
|
||||||
|
isnew: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Returns platform according to the prop.
|
||||||
|
* @returns {Platform}
|
||||||
|
*/
|
||||||
|
getPlatformInfo(): Platform {
|
||||||
|
if (!this.platform) return {}
|
||||||
|
|
||||||
|
const platform = this.platform.toLowerCase()
|
||||||
|
let classes
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case 'netflix':
|
||||||
|
classes = 'text-red-600 bg-black'
|
||||||
|
break
|
||||||
|
case 'fox':
|
||||||
|
classes = 'text-gray-100 bg-red-500'
|
||||||
|
break
|
||||||
|
case 'apple tv+':
|
||||||
|
classes = 'text-white bg-black'
|
||||||
|
break
|
||||||
|
case 'tnt':
|
||||||
|
classes = 'text-white bg-red-600'
|
||||||
|
break
|
||||||
|
case 'amazon-prime':
|
||||||
|
classes = 'text-gray-100 bg-blue-500'
|
||||||
|
break
|
||||||
|
case 'disney+':
|
||||||
|
classes = 'text-white bg-blue-900'
|
||||||
|
break
|
||||||
|
case 'adult-swim':
|
||||||
|
classes = 'text-gray-100 bg-black'
|
||||||
|
break
|
||||||
|
case 'bbc':
|
||||||
|
classes = 'text-gray-100 bg-black'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
classes = 'bg-gray-200 dark:bg-neutral-800'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
platform,
|
||||||
|
classes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center space-x-2 truncate">
|
||||||
|
<div class="flex items-center flex-shrink-0 space-x-1">
|
||||||
|
<!-- Channel Icon -->
|
||||||
|
<IconChannel
|
||||||
|
v-tippy="{
|
||||||
|
content: platform,
|
||||||
|
placement: 'top'
|
||||||
|
}"
|
||||||
|
:platform="getPlatformInfo.platform"
|
||||||
|
class="flex-shrink-0 w-6 h-6 p-1 rounded-full focus:outline-none"
|
||||||
|
:class="getPlatformInfo.classes"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-tippy="{
|
||||||
|
content: `${rating}/${max} puan`,
|
||||||
|
placement: 'top'
|
||||||
|
}"
|
||||||
|
class="rounded-md cursor-default flex font-medium bg-gray-200 flex-shrink-0 text-sm p-1 text-gray-700 w-12 items-center justify-center dark:(bg-neutral-800 text-gray-200) focus:outline-none"
|
||||||
|
>
|
||||||
|
{{ rating }} P
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-gray-900 truncate dark:text-gray-100" :class="{ new: isnew }">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
a {
|
||||||
|
@apply border-b border-black/10 transition-colors dark:(border-white/10 hover:border-white/30) hover:border-black/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new a {
|
||||||
|
@apply border-blue-500 border-b-2 hover:border-blue-800;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
100
src/components/Blog/ReadingIndicator.vue
Executable file
100
src/components/Blog/ReadingIndicator.vue
Executable file
@ -0,0 +1,100 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
selector: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
el: null as Element | null,
|
||||||
|
scrollY: 0,
|
||||||
|
rect: {
|
||||||
|
top: 0,
|
||||||
|
bottom: 0
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
height: 0,
|
||||||
|
width: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Calculates the position of the element and returns percentage value.
|
||||||
|
*/
|
||||||
|
getPercentLeftBottom(): number {
|
||||||
|
const { top, bottom } = this.rect
|
||||||
|
const percent = Math.round(((top - this.window.height) / (top - bottom)) * 100)
|
||||||
|
|
||||||
|
return percent > 100 ? 100 : percent
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Checks if the position is higher than a specific number and returns a boolean value.
|
||||||
|
*/
|
||||||
|
isElementVisible(): boolean {
|
||||||
|
return this.scrollY > 175
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// Find element in the document and set if exists
|
||||||
|
const element = document.querySelector(this.selector)
|
||||||
|
if (element) this.el = element
|
||||||
|
else return
|
||||||
|
|
||||||
|
// Set window dimensions
|
||||||
|
const { innerHeight, innerWidth } = window
|
||||||
|
this.window = { height: innerHeight, width: innerWidth }
|
||||||
|
|
||||||
|
// Add scroll event to update positions
|
||||||
|
window.addEventListener('scroll', this.handleScroll)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
// Remove scroll event before changing the page
|
||||||
|
window.removeEventListener('scroll', this.handleScroll)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleScroll() {
|
||||||
|
// Set currenc scroll position
|
||||||
|
this.scrollY = window.scrollY
|
||||||
|
|
||||||
|
// Set window height and width
|
||||||
|
const { innerHeight, innerWidth } = window
|
||||||
|
this.window = { height: innerHeight, width: innerWidth }
|
||||||
|
|
||||||
|
// Get element's position
|
||||||
|
const { top, bottom } = this.el?.getBoundingClientRect() || {}
|
||||||
|
|
||||||
|
// Save element's position to Vue data
|
||||||
|
if (!top || !bottom) return
|
||||||
|
this.rect = { top, bottom }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<transition name="fade">
|
||||||
|
<div
|
||||||
|
v-show="isElementVisible"
|
||||||
|
v-tippy="{
|
||||||
|
content: getPercentLeftBottom === 100 ? 'Tüm yazı okundu!' : 'Okuma oranı'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="rounded-md bg-gray-200 h-40 w-2 hidden relative md:block dark:bg-neutral-800">
|
||||||
|
<div
|
||||||
|
class="rounded-md inset-x-0 transition bottom-0 absolute"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500': getPercentLeftBottom === 100,
|
||||||
|
'bg-gray-300 dark:bg-neutral-600': getPercentLeftBottom < 100
|
||||||
|
}"
|
||||||
|
:style="{ height: `${getPercentLeftBottom}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
239
src/components/Blog/Sections/Ratings.vue
Executable file
239
src/components/Blog/Sections/Ratings.vue
Executable file
@ -0,0 +1,239 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ratings: [
|
||||||
|
{
|
||||||
|
name: 'Daredevil',
|
||||||
|
rating: 10,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#daredevil'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Prison Break',
|
||||||
|
rating: 10,
|
||||||
|
platform: 'Fox',
|
||||||
|
anchor: '#prison-break'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Arcane',
|
||||||
|
rating: 10,
|
||||||
|
platform: 'Netflix',
|
||||||
|
isNew: true,
|
||||||
|
anchor: '#arcane'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Love, Death & Robots',
|
||||||
|
rating: 10,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#love-death--robots'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Élite',
|
||||||
|
rating: 9.5,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#élite'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'The Witcher',
|
||||||
|
rating: 9.5,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#the-witcher'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'The Boys',
|
||||||
|
rating: 9.5,
|
||||||
|
platform: 'Amazon Prime',
|
||||||
|
anchor: '#the-boys'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rise of Empires: Ottoman',
|
||||||
|
rating: 9.5,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#rise-of-empires-ottoman'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'The Mandalorian',
|
||||||
|
rating: 9.5,
|
||||||
|
platform: 'Disney+',
|
||||||
|
anchor: '#the-mandalorian'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'La Casa de Papel',
|
||||||
|
rating: 9.5,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#la-casa-de-papel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sex Education',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#sex-education'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Locke & Key',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#locke--key'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Stranger Things',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#stranger-things'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'See',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Apple TV+',
|
||||||
|
anchor: '#see'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sherlock',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'BBC',
|
||||||
|
anchor: '#sherlock'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Loki',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Disney+',
|
||||||
|
isNew: true,
|
||||||
|
anchor: '#loki'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Lupin',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#lupin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Snowpiercer',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'TNT',
|
||||||
|
anchor: '#snowpiercer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'The Haunting of Bly Manor',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#the-haunting-of-bly-manor'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'What If...?',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Disney+',
|
||||||
|
isNew: true,
|
||||||
|
anchor: '#what-if'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'When They See Us',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#when-they-see-us'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sense8',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#sense8'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Chilling Adventures of Sabrina',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#chilling-adventures-of-sabrina'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Altered Carbon',
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#altered-carbon'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "The Queen's Gambit",
|
||||||
|
rating: 9,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#the-queens-gambit'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Aşk 101',
|
||||||
|
rating: 8.5,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#aşk-101'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'The Order',
|
||||||
|
rating: 8.5,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#the-order'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'BoJack Horseman',
|
||||||
|
rating: 8.5,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#bojack-horseman'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rick and Morty',
|
||||||
|
rating: 8.5,
|
||||||
|
platform: 'Adult Swim',
|
||||||
|
anchor: '#rick-and-morty'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Lost in Space',
|
||||||
|
rating: 8.5,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#lost-in-space'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'The Haunting of Hill House',
|
||||||
|
rating: 8,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#the-haunting-of-hill-house'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'You',
|
||||||
|
rating: 8,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#you'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Lucifer',
|
||||||
|
rating: 8,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#lucifer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'The Umbrella Academy',
|
||||||
|
rating: 8,
|
||||||
|
platform: 'Netflix',
|
||||||
|
anchor: '#the-umbrella-academy'
|
||||||
|
}
|
||||||
|
].sort((a, b) => b.rating - a.rating)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="grid gap-2 mb-6 lg:grid-cols-2">
|
||||||
|
<BlogRating
|
||||||
|
v-for="item in ratings"
|
||||||
|
:key="item.name"
|
||||||
|
:rating="item.rating"
|
||||||
|
:platform="item.platform"
|
||||||
|
:isnew="item.isNew"
|
||||||
|
>
|
||||||
|
<a :href="item.anchor">{{ item.name }}</a>
|
||||||
|
</BlogRating>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
a {
|
||||||
|
@apply text-current font-normal no-underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
100
src/components/Blog/Share.vue
Executable file
100
src/components/Blog/Share.vue
Executable file
@ -0,0 +1,100 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
copied: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Creates a window or copies the URL.
|
||||||
|
* @param {'url'|'twitter'|'telegram'|'whatsapp'} option The share option.
|
||||||
|
*/
|
||||||
|
share(option: 'url' | 'twitter' | 'telegram' | 'whatsapp') {
|
||||||
|
if (option === 'url') {
|
||||||
|
let el = this.$refs['share-url'] as HTMLInputElement
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement('input')
|
||||||
|
|
||||||
|
el.value = this.path ? `https://toshiki.dev${this.path}` : location.href
|
||||||
|
document.body.appendChild(el)
|
||||||
|
|
||||||
|
el.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(el)
|
||||||
|
} else {
|
||||||
|
el.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.copied = true
|
||||||
|
setTimeout(() => (this.copied = false), 3000)
|
||||||
|
} else {
|
||||||
|
let url = ''
|
||||||
|
|
||||||
|
switch (option) {
|
||||||
|
case 'twitter':
|
||||||
|
url = `https://twitter.com/intent/tweet?via=andatoshiki&text=${encodeURIComponent(
|
||||||
|
this.title + '\n' + location.href
|
||||||
|
)}`
|
||||||
|
break
|
||||||
|
case 'telegram':
|
||||||
|
url = `https://telegram.me/share/url?url=${encodeURIComponent(location.href)}`
|
||||||
|
break
|
||||||
|
case 'whatsapp':
|
||||||
|
url = `https://api.whatsapp.com/send?text=${encodeURIComponent(
|
||||||
|
this.title + '\n' + location.href
|
||||||
|
)}`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(url, `${option[0].toUpperCase() + option.toLowerCase().slice(1)}`, 'width=400,height=550')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<Button rounded @click.native="share('twitter')">
|
||||||
|
<IconBrand brand="twitter" class="text-[#1DA1F2] h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button rounded @click.native="share('telegram')">
|
||||||
|
<IconBrand brand="telegram" class="text-[#2EAADE] h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button rounded @click.native="share('whatsapp')">
|
||||||
|
<IconBrand brand="whatsapp" class="text-[#25D366] h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button rounded @click.native="share('url')">
|
||||||
|
<IconCheck v-if="copied === true" class="text-green-500 h-6 w-6" />
|
||||||
|
<IconLink v-else class="text-gray-800 dark:text-gray-200 h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<input ref="share-url" readonly :value="`https://toshiki.dev${path}`" class="hidden" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.btn svg {
|
||||||
|
@apply h-8 w-8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
src/components/Blog/TableOfContents.vue
Executable file
49
src/components/Blog/TableOfContents.vue
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue, { PropType } from 'vue'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { Toc } from '~/src/types/Post'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
toc: {
|
||||||
|
type: Array as PropType<Toc[]>,
|
||||||
|
required: true,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tocToggled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="toc && toc.length > 0" class="rounded-md flex flex-col space-y-2 mb-6">
|
||||||
|
<div
|
||||||
|
class="cursor-pointer flex font-medium space-x-1 text-sm transition-colors text-gray-500 items-center dark:text-dark-100 hover:text-gray-700 dark:hover:text-white/40 select-none"
|
||||||
|
@click="tocToggled = !tocToggled"
|
||||||
|
>
|
||||||
|
<h1 class="uppercase">Titile</h1>
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<IconChevron v-if="!tocToggled" key="'tocToggled'" right class="h-4 w-4" />
|
||||||
|
|
||||||
|
<IconChevron v-else key="'tocToggledFalse'" down class="h-4 w-4" />
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul v-show="tocToggled === true" class="flex flex-wrap gap-2 items-center">
|
||||||
|
<li
|
||||||
|
v-for="link of toc || []"
|
||||||
|
:key="link.id"
|
||||||
|
class="border-b border-gray-300 text-sm transition-colors text-dark-400 dark:border-dark-200 dark:text-white/30 hover:text-dark-700 dark:hover:text-white/60"
|
||||||
|
>
|
||||||
|
<a v-if="link.id" :href="`#${link.id}`">
|
||||||
|
{{ link.text }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
66
src/components/Button.vue
Executable file
66
src/components/Button.vue
Executable file
@ -0,0 +1,66 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue, { PropType } from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
// String
|
||||||
|
href: {
|
||||||
|
type: [] as PropType<any>,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
|
||||||
|
// Boolean
|
||||||
|
block: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
rounded: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
blank: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getIconName(): string {
|
||||||
|
return this.icon?.startsWith('Icon') ? this.icon : `Icon${this.icon}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SmartLink
|
||||||
|
:href="!disabled && href"
|
||||||
|
:blank="blank"
|
||||||
|
class="cursor-pointer justify-center px-5 py-2 rounded-lg card-base flex items-center space-x-2"
|
||||||
|
:class="{
|
||||||
|
'w-max': !block,
|
||||||
|
'rounded-full': rounded
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<component :is="getIconName" v-if="icon && !$slots.icon" class="h-4 w-4" />
|
||||||
|
|
||||||
|
<slot v-else name="icon" />
|
||||||
|
|
||||||
|
<span v-if="$slots.default">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</SmartLink>
|
||||||
|
</template>
|
||||||
303
src/components/Card/Discord.vue
Executable file
303
src/components/Card/Discord.vue
Executable file
@ -0,0 +1,303 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/* eslint-disable no-undef */
|
||||||
|
|
||||||
|
import Vue, { PropType } from 'vue'
|
||||||
|
|
||||||
|
/* Import image files */
|
||||||
|
import largeImages from '@/assets/files/premid/largeImages'
|
||||||
|
import smallImages from '@/assets/files/premid/smallImages'
|
||||||
|
|
||||||
|
/* Interfaces */
|
||||||
|
interface ImageCategory {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'Custom Status'
|
||||||
|
},
|
||||||
|
largeImage: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'PreMiD'
|
||||||
|
},
|
||||||
|
smallImage: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
smallImageText: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
type: Array as PropType<{ label: string; url: string }[]>,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
customImageUrl: {
|
||||||
|
type: Object as PropType<{ small: string; large: string }>,
|
||||||
|
required: false,
|
||||||
|
default: () => ({ small: '', large: '' })
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
componentReady: false,
|
||||||
|
timers: {
|
||||||
|
elapsed: {
|
||||||
|
instance: null as NodeJS.Timeout | null,
|
||||||
|
string: ''
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
instance: null as NodeJS.Timeout | null,
|
||||||
|
string: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Returns large and small image by replacing the spaces in their name.
|
||||||
|
* @returns {{largeImage: string, smallImage: string}}
|
||||||
|
*/
|
||||||
|
getImages(): { largeImage: string; smallImage: string | null } {
|
||||||
|
const { largeImage, smallImage } = this
|
||||||
|
|
||||||
|
/* Map arrays and combine items in all categories */
|
||||||
|
const largeAll: ImageCategory[] = []
|
||||||
|
const smallAll: ImageCategory[] = []
|
||||||
|
|
||||||
|
/* Loop into all arrays inside items and combine them in a single array */
|
||||||
|
largeImages.map(item => item.items).forEach(category => largeAll.push(...category))
|
||||||
|
|
||||||
|
smallImages.map(item => item.items).forEach(category => smallAll.push(...category))
|
||||||
|
|
||||||
|
return {
|
||||||
|
largeImage: this.customImageUrl.large
|
||||||
|
? largeImage
|
||||||
|
: largeAll.find(item => item.name === largeImage)?.url || 'https://i.imgur.com/CuVtvKW.png',
|
||||||
|
smallImage: this.customImageUrl.small
|
||||||
|
? smallImage
|
||||||
|
: smallAll.find(item => item.name === smallImage)?.url || null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Returns text related parts for the UI.
|
||||||
|
* @returns {{details: string, state: string, small: string | undefined}}
|
||||||
|
*/
|
||||||
|
getText(): { details: string; state: string; small: string | undefined } {
|
||||||
|
const { smallImage, smallImageText, details, state } = this
|
||||||
|
|
||||||
|
let small
|
||||||
|
|
||||||
|
if (smallImage && smallImageText) small = smallImageText
|
||||||
|
else if (smallImage && !smallImageText) small = '[EMPTY]'
|
||||||
|
|
||||||
|
return {
|
||||||
|
details,
|
||||||
|
state,
|
||||||
|
small
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Checks if timers are enabled, starts or stops timers according to passed props.
|
||||||
|
* @returns {boolean} Whether any timer is enabled or not.
|
||||||
|
*/
|
||||||
|
isTimerEnabled(): boolean {
|
||||||
|
const start = this?.timestamp?.start
|
||||||
|
const end = this?.timestamp?.end
|
||||||
|
|
||||||
|
if (start?.enabled && start?.value) {
|
||||||
|
this.startElapsedTimer()
|
||||||
|
return true
|
||||||
|
} else if (end?.enabled && end?.value) {
|
||||||
|
this.startLeftTimer()
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
this.stopTimers()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Returns the string for enabled timer.
|
||||||
|
* @returns {boolean |null | string}
|
||||||
|
*/
|
||||||
|
getTime(): boolean | null | string {
|
||||||
|
if (this.isTimerEnabled === false) return null
|
||||||
|
else if (this.timers.elapsed?.instance) return this.timers.elapsed.string
|
||||||
|
else if (this.timers.end?.instance) return this.timers.end.string
|
||||||
|
else return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.componentReady = true
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopTimers()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Stops both of the timers.
|
||||||
|
*/
|
||||||
|
stopTimers() {
|
||||||
|
const { elapsed, end } = this.timers
|
||||||
|
|
||||||
|
if (typeof elapsed === 'boolean' && typeof end === 'boolean') return
|
||||||
|
|
||||||
|
/* Clear elapsed timer */
|
||||||
|
|
||||||
|
// @ts-ignore-next-line
|
||||||
|
clearInterval(elapsed.instance)
|
||||||
|
elapsed.instance = null
|
||||||
|
elapsed.string = ''
|
||||||
|
|
||||||
|
/* Clear end timer */
|
||||||
|
|
||||||
|
// @ts-ignore-next-line
|
||||||
|
clearInterval(end.instance)
|
||||||
|
end.instance = null
|
||||||
|
end.string = ''
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Calculates the time difference between now and selected time and starts the elapsed timer.
|
||||||
|
*/
|
||||||
|
startElapsedTimer() {
|
||||||
|
const target = this?.timestamp?.start?.value
|
||||||
|
const timer = this?.timers?.elapsed
|
||||||
|
|
||||||
|
if (!target || !timer) return
|
||||||
|
this.stopTimers()
|
||||||
|
|
||||||
|
timer.string = '00:00 elapsed'
|
||||||
|
timer.instance = setInterval(() => {
|
||||||
|
let timeArray = [
|
||||||
|
String(this.$moment().diff(target, 'hours')),
|
||||||
|
String(this.$moment().diff(target, 'minutes') % 60),
|
||||||
|
String(this.$moment().diff(target, 'seconds') % 60)
|
||||||
|
]
|
||||||
|
|
||||||
|
if (timeArray[0] === '0') timeArray = timeArray.slice(1)
|
||||||
|
timeArray = timeArray.map(time => (time.length === 1 ? `0${time}` : time))
|
||||||
|
|
||||||
|
timer.string = `${timeArray.join(':')} elapsed`
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Calculates the time difference between now and selected time and starts the elapsed timer.
|
||||||
|
*/
|
||||||
|
startLeftTimer() {
|
||||||
|
const target = this?.timestamp?.end?.value
|
||||||
|
const timer = this?.timers?.end
|
||||||
|
|
||||||
|
if (!target || !timer) return
|
||||||
|
this.stopTimers()
|
||||||
|
|
||||||
|
timer.string = '--:-- left'
|
||||||
|
timer.instance = setInterval(() => {
|
||||||
|
const toTime = this.$moment(target, 'HH:mm').unix()
|
||||||
|
const fromTime = this.$moment().unix()
|
||||||
|
const duration = this.$moment.duration(toTime - fromTime, 'seconds')
|
||||||
|
|
||||||
|
if (duration.asSeconds() < 0) return (timer.string = '00:00 left')
|
||||||
|
|
||||||
|
let timeArray = [String(duration.hours()), String(duration.minutes()), String(duration.seconds())]
|
||||||
|
|
||||||
|
if (timeArray[0] === '0') timeArray = timeArray.slice(1)
|
||||||
|
timeArray = timeArray.map(time => (time.length === 1 ? `0${time}` : time))
|
||||||
|
|
||||||
|
timer.string = `${timeArray.join(':')} left`
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="componentReady"
|
||||||
|
class="rounded-md bg-[#6c82cf] w-full py-4 px-6 overflow-x-hidden dark:bg-neutral-800/40"
|
||||||
|
>
|
||||||
|
<div class="pt-2">
|
||||||
|
<h1 class="font-semibold text-xs text-white uppercase dark:text-gray-100">Playing a game</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-col space-y-3 items-center justify-between overflow-x-hidden md:(space-y-0 space-x-3 flex-row)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex space-x-3 w-full py-2 items-center overflow-x-hidden md:space-x-5"
|
||||||
|
:class="buttons.length > 0 && 'md:w-2/3'"
|
||||||
|
>
|
||||||
|
<div class="flex-shrink-0 h-32 w-32 relative">
|
||||||
|
<SmartImage
|
||||||
|
:key="getImages.largeImage"
|
||||||
|
:src="getImages.largeImage"
|
||||||
|
class="rounded-xl"
|
||||||
|
alt="large image"
|
||||||
|
height="256"
|
||||||
|
width="256"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SmartImage
|
||||||
|
v-if="getImages.smallImage"
|
||||||
|
:key="getImages.smallImage"
|
||||||
|
v-tippy="{
|
||||||
|
content: getText.small,
|
||||||
|
placement: 'top'
|
||||||
|
}"
|
||||||
|
:src="getImages.smallImage"
|
||||||
|
alt="small image"
|
||||||
|
class="rounded-full bg-[#6c82cf] h-9 -right-2 -bottom-2 ring-4 ring-[#6c82cf] w-9 overflow-y-hidden absolute box-border dark:(bg-transparent ring-transparent) focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-gray-100 overflow-x-hidden">
|
||||||
|
<h1 class="font-medium text-xl text-white block">{{ title }}</h1>
|
||||||
|
|
||||||
|
<div class="leading-tight">
|
||||||
|
<span class="block truncate">{{ getText.details }}</span>
|
||||||
|
<span class="block truncate">{{ getText.state }}</span>
|
||||||
|
|
||||||
|
<span v-if="isTimerEnabled === true" class="text-sm block">{{ getTime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="buttons.length > 0" class="flex flex-col space-y-2 flex-shrink-0 md:w-1/3">
|
||||||
|
<div v-for="(button, index) in buttons" :key="`button-${index}`" class="flex justify-end">
|
||||||
|
<SmartLink
|
||||||
|
:href="button.url"
|
||||||
|
:title="button.url"
|
||||||
|
class="border rounded-sm cursor-pointer border-white/40 text-sm py-2 px-4 transition-colors text-gray-300 truncate select-none md:(px-3 py-1) hover:(text-white border-white) focus:(bg-opacity-10 bg-white)"
|
||||||
|
blank
|
||||||
|
>{{ button.label }}</SmartLink
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skeleton load -->
|
||||||
|
<div v-else class="rounded-md bg-[#6c82cf] h-[12.5rem] w-full animate-pulse" />
|
||||||
|
</template>
|
||||||
54
src/components/Card/Experience.vue
Executable file
54
src/components/Card/Experience.vue
Executable file
@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: new Date().getFullYear()
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
hiddenBadge: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SmartLink :href="url" blank>
|
||||||
|
<div class="card-base leading-relaxed rounded-lg">
|
||||||
|
<div class="flex space-x-2 items-center justify-between">
|
||||||
|
<h3>{{ title }}</h3>
|
||||||
|
|
||||||
|
<span class="text-black/50 dark:text-white/30 text-sm">{{ date }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="position"
|
||||||
|
class="truncate text-sm text-black/50 dark:text-white/30"
|
||||||
|
:class="hiddenBadge && 'flex items-center justify-between'"
|
||||||
|
>
|
||||||
|
{{ position }}
|
||||||
|
<IconPlus v-if="hiddenBadge" class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SmartLink>
|
||||||
|
</template>
|
||||||
92
src/components/Card/Index.vue
Executable file
92
src/components/Card/Index.vue
Executable file
@ -0,0 +1,92 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue, { PropType } from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
href: {
|
||||||
|
type: [] as PropType<any>,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
tight: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
elevated: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
truncate: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
classes: 'rounded-md overflow-x-hidden transition-colors'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="href ? 'SmartLink' : 'div'"
|
||||||
|
:href="href"
|
||||||
|
class="rounded-lg card-base"
|
||||||
|
:class="{
|
||||||
|
[classes]: true,
|
||||||
|
'p-2': tight === true,
|
||||||
|
'p-4': tight === false,
|
||||||
|
'cursor-pointer': cursor === true,
|
||||||
|
'items-center flex space-x-4': $slots.icon || $slots['icon-left'],
|
||||||
|
'justify-between': $slots.icon && !$slots['icon-left']
|
||||||
|
}"
|
||||||
|
v-bind="href ? $attrs : false"
|
||||||
|
>
|
||||||
|
<div v-if="$slots['icon-left']" class="flex-shrink-0">
|
||||||
|
<slot name="icon-left" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-hidden leading-relaxed space-y-2">
|
||||||
|
<h2 v-if="title" class="font-medium text-black dark:text-white truncate">
|
||||||
|
{{ title }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p
|
||||||
|
v-if="$slots.default"
|
||||||
|
class="text-black/50 dark:text-white/30 text-sm"
|
||||||
|
:class="truncate === true && 'line-clamp-2'"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.icon" class="flex-shrink-0">
|
||||||
|
<slot name="icon" />
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
76
src/components/Card/LastFm.vue
Executable file
76
src/components/Card/LastFm.vue
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
artist: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
nowPlaying: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
plays: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SmartLink
|
||||||
|
:href="url"
|
||||||
|
:title="name"
|
||||||
|
class="rounded-lg flex items-center gap-4 card-base"
|
||||||
|
:class="{
|
||||||
|
'justify-between': plays !== null
|
||||||
|
}"
|
||||||
|
blank
|
||||||
|
>
|
||||||
|
<div class="flex space-x-4 truncate items-center">
|
||||||
|
<div class="flex-shrink-0 h-14 w-14 relative">
|
||||||
|
<SmartImage :src="image" class="rounded-md" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="nowPlaying"
|
||||||
|
title="Playing now..."
|
||||||
|
class="rounded-md flex bg-black/75 inset-0 items-center justify-center absolute"
|
||||||
|
>
|
||||||
|
<IconPlay class="h-6 text-neutral-300 w-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col truncate">
|
||||||
|
<span class="truncate">
|
||||||
|
{{ name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="artist" class="text-sm text-black/50 dark:text-white/30 truncate">by {{ artist }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="plays"
|
||||||
|
class="rounded-md text-blue-600 bg-blue-600/10 ring-[0.5px] ring-blue-600/40 px-4 py-1 flex-shrink-0 text-xs"
|
||||||
|
>
|
||||||
|
{{ plays }} plays
|
||||||
|
</div>
|
||||||
|
</SmartLink>
|
||||||
|
</template>
|
||||||
68
src/components/Card/Post/Index.vue
Executable file
68
src/components/Card/Post/Index.vue
Executable file
@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue, { PropType } from 'vue'
|
||||||
|
|
||||||
|
/* Interfaces */
|
||||||
|
import type { Post } from '@/types/Post'
|
||||||
|
|
||||||
|
export interface PostMeta {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
slug?: string
|
||||||
|
special?: boolean
|
||||||
|
tag?: string
|
||||||
|
image?: string
|
||||||
|
date?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
post: {
|
||||||
|
type: Object as PropType<Post>,
|
||||||
|
required: true,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'normal'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hovered: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Returns post meta safely.
|
||||||
|
* @returns {PostMeta |null}
|
||||||
|
*/
|
||||||
|
getPostMeta(): PostMeta {
|
||||||
|
if (!this.post) return {}
|
||||||
|
|
||||||
|
const image = this.post?.image || `/assets/images/posts/${this.post?.slug}.jpg` || ''
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: this.post.title || '',
|
||||||
|
description: this.post.description || '',
|
||||||
|
slug: this.post.slug || '',
|
||||||
|
special: this.post.special || false,
|
||||||
|
tag: this.post?.tags?.[0] || '',
|
||||||
|
date: this.post?.createdAt,
|
||||||
|
image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Normal -->
|
||||||
|
<CardPostNormal v-if="type === 'normal'" :meta="getPostMeta" />
|
||||||
|
|
||||||
|
<!-- Text -->
|
||||||
|
<CardPostText v-else-if="type === 'text'" :meta="getPostMeta" />
|
||||||
|
|
||||||
|
<!-- Text and Title -->
|
||||||
|
<CardPostTextTitle v-else-if="type === 'text-only-title'" :meta="getPostMeta" />
|
||||||
|
</template>
|
||||||
54
src/components/Card/Post/Normal.vue
Executable file
54
src/components/Card/Post/Normal.vue
Executable file
@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue, { PropType } from 'vue'
|
||||||
|
|
||||||
|
// Import type
|
||||||
|
import type { PostMeta } from './Index.vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
meta: {
|
||||||
|
type: Object as PropType<PostMeta>,
|
||||||
|
required: true,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hovered: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="meta" class="overflow-hidden" @mouseover="hovered = true" @mouseleave="hovered = false">
|
||||||
|
<SmartLink
|
||||||
|
:title="meta.title"
|
||||||
|
:href="{
|
||||||
|
name: 'blog-slug',
|
||||||
|
params: { slug: meta.slug }
|
||||||
|
}"
|
||||||
|
class="rounded-lg cursor-pointer space-y-2 focus-ring"
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<SmartImage :src="meta.image" class="rounded h-40 w-full filter dark:brightness-75" />
|
||||||
|
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<div v-show="hovered" class="flex bg-black/50 inset-0 absolute items-center justify-center">
|
||||||
|
<IconLink class="h-6 text-white w-6" />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-1">
|
||||||
|
<h2 class="font-medium text-lg leading-tight text-gray-700 truncate dark:text-gray-200 hover:underline">
|
||||||
|
{{ meta.title }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-neutral-500 line-clamp-2">
|
||||||
|
{{ meta.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SmartLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
40
src/components/Card/Post/Text.vue
Executable file
40
src/components/Card/Post/Text.vue
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue, { PropType } from 'vue'
|
||||||
|
|
||||||
|
// Import type
|
||||||
|
import type { PostMeta } from './Index.vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
meta: {
|
||||||
|
type: Object as PropType<PostMeta>,
|
||||||
|
required: true,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SmartLink
|
||||||
|
v-if="meta"
|
||||||
|
:title="meta.title"
|
||||||
|
:href="{
|
||||||
|
name: 'blog-slug',
|
||||||
|
params: { slug: meta.slug }
|
||||||
|
}"
|
||||||
|
class="rounded-lg cursor-pointer flex space-x-4 p-3 transition-colors focus-ring items-center md:px-4 hover:bg-gray-200/40 dark:hover:bg-neutral-800/40"
|
||||||
|
>
|
||||||
|
<SmartImage :src="meta.image" class="rounded flex-shrink-0 h-20 w-24 filter dark:brightness-75" />
|
||||||
|
|
||||||
|
<div class="flex flex-col overflow-x-hidden">
|
||||||
|
<h2 class="font-medium text-lg text-gray-800 truncate dark:text-gray-200">
|
||||||
|
{{ meta.title }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-neutral-500 line-clamp-2">
|
||||||
|
{{ meta.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SmartLink>
|
||||||
|
</template>
|
||||||
53
src/components/Card/Post/TextTitle.vue
Executable file
53
src/components/Card/Post/TextTitle.vue
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue, { PropType } from 'vue'
|
||||||
|
// Import type
|
||||||
|
import type { PostMeta } from './Index.vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
meta: {
|
||||||
|
type: Object as PropType<PostMeta>,
|
||||||
|
required: true,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getPostDate(): string | null {
|
||||||
|
if (!this.meta || !this.meta.date) return null
|
||||||
|
return this.$getReadableDate(this.meta.date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SmartLink
|
||||||
|
v-if="meta"
|
||||||
|
:title="meta.title"
|
||||||
|
:href="{
|
||||||
|
name: 'blog-slug',
|
||||||
|
params: { slug: meta.slug }
|
||||||
|
}"
|
||||||
|
class="rounded-lg cursor-pointer flex flex-col p-3 px-4 transition-colors focus-ring truncate hover:bg-gray-200/40 dark:hover:bg-neutral-800/40"
|
||||||
|
>
|
||||||
|
<h2 class="font-medium text-lg text-gray-800 truncate dark:text-gray-200">
|
||||||
|
{{ meta.title }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="flex space-x-1 items-center">
|
||||||
|
<IconFire
|
||||||
|
v-if="meta.special"
|
||||||
|
v-tippy="{
|
||||||
|
content: 'Popüler gönderi',
|
||||||
|
placement: 'bottom'
|
||||||
|
}"
|
||||||
|
class="flex-shrink-0 h-5 text-red-600 w-5 dark:text-red-500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex space-x-2 text-gray-700 truncate items-center dark:text-gray-400">
|
||||||
|
<IconClock class="flex-shrink-0 h-5 w-5" />
|
||||||
|
<span class="truncate">{{ getPostDate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SmartLink>
|
||||||
|
</template>
|
||||||
84
src/components/Card/Repository.vue
Executable file
84
src/components/Card/Repository.vue
Executable file
@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
stars: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
top: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Returns proper name for the language icon.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getLanguageIcon(): string {
|
||||||
|
const icons = {
|
||||||
|
Vue: 'Vue.js'
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore-next-line
|
||||||
|
return icons[this.language] || this.language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-lg card-base">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div :class="top && 'flex justify-between space-x-2'">
|
||||||
|
<h3 class="text-black/90 dark:text-white/90 items-center truncate space-x-1">
|
||||||
|
<span class="text-black/50 dark:text-white/30">@andatoshiki/</span><span>{{ name }}</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<IconStar v-if="top === true" class="h-6 text-yellow-600 w-6" title="Top repository" filled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-black/50 dark:text-white/30 line-clamp-2">
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="flex items-center justify-between text-black/50 dark:text-white/30">
|
||||||
|
<span>Stars:</span>
|
||||||
|
<span>{{ stars }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between text-black/50 dark:text-white/30">
|
||||||
|
<span>Language:</span>
|
||||||
|
<IconDev :brand="getLanguageIcon" class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="license" class="flex items-center justify-between text-black/50 dark:text-white/30">
|
||||||
|
<span>License:</span>
|
||||||
|
<span>{{ license }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
36
src/components/Card/Skill.vue
Executable file
36
src/components/Card/Skill.vue
Executable file
@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
iconPack: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'IconDev'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card-base rounded-lg flex items-center space-x-4">
|
||||||
|
<div class="rounded-lg flex">
|
||||||
|
<SmartImage v-if="image" :src="image" class="h-5 w-5 flex-shrink-0" />
|
||||||
|
<component v-else :is="iconPack" :brand="title" class="flex-shrink-0 h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="flex-1 truncate text-sm">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
67
src/components/Card/Song.vue
Executable file
67
src/components/Card/Song.vue
Executable file
@ -0,0 +1,67 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
type: [String, Date],
|
||||||
|
required: true,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
thumbnail: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Compares the dates between the provided date and current date and returns a title which will be used in cards' title.
|
||||||
|
* @returns {string} The title "Today's Song" or formatted date.
|
||||||
|
*/
|
||||||
|
getDateText(): string {
|
||||||
|
if (
|
||||||
|
this.$moment(this.date).utcOffset(3).format('DD/MM/YYYY') ===
|
||||||
|
this.$moment(this.$getArizonaTime()).format('DD/MM/YYYY')
|
||||||
|
)
|
||||||
|
return "Today's Song"
|
||||||
|
else return this.$moment(this.date).utcOffset(3).format('DD/MM/YYYY')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-lg cursor-pointer card-base flex flex-col space-y-2">
|
||||||
|
<div class="rounded-md flex-shrink-0">
|
||||||
|
<SmartImage
|
||||||
|
:src="thumbnail"
|
||||||
|
fit="outside"
|
||||||
|
class="rounded-md max-w-full max-h-full h-16 w-16"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1 truncate">
|
||||||
|
<h3 class="font-medium flex-shrink-0 leading-tight truncate">
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="flex space-x-1 text-sm items-center text-black/50 dark:text-white/30">
|
||||||
|
<IconStar v-if="getDateText.startsWith('Today')" class="flex-shrink-0 h-4 w-4" />
|
||||||
|
|
||||||
|
<IconCalendar class="h-4 w-4" />
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ getDateText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
33
src/components/Card/Sponsor.vue
Executable file
33
src/components/Card/Sponsor.vue
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { Sponsor } from '~/src/types/Response/Sponsors'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
sponsor: {
|
||||||
|
type: Object as () => Sponsor,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
monthly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SmartLink :href="`https://github.com/${sponsor.login}`" class="card-base rounded-lg flex flex-col gap-2" blank>
|
||||||
|
<SmartImage :src="sponsor.avatarUrl" class="h-10 w-10 flex-shrink-0 rounded-full" />
|
||||||
|
|
||||||
|
<div class="flex overflow-x-hidden flex-col leading-tight">
|
||||||
|
<span class="truncate">{{ sponsor.login }}</span>
|
||||||
|
<span class="text-sm text-black/30 dark:text-white/30 truncate">
|
||||||
|
{{ monthly ? 'Monthly' : 'One time' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SmartLink>
|
||||||
|
</template>
|
||||||
27
src/components/ColorSwitcher.vue
Executable file
27
src/components/ColorSwitcher.vue
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Returns the selected color mode value.
|
||||||
|
* @returns {string} The color mode as "light" or "dark".
|
||||||
|
*/
|
||||||
|
getSelectedTheme(): string {
|
||||||
|
return this.$colorMode.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Updates the color mode value.
|
||||||
|
*/
|
||||||
|
switchTheme() {
|
||||||
|
this.$colorMode.preference = this.getSelectedTheme === 'dark' ? 'light' : 'dark'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button rounded elevated :icon="getSelectedTheme === 'light' ? 'Sun' : 'Moon'" @click.native="switchTheme" />
|
||||||
|
</template>
|
||||||
127
src/components/DPlayer.vue
Normal file
127
src/components/DPlayer.vue
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="dplayer" class="rounded-md"></div>
|
||||||
|
</template>
|
||||||
|
<!-- Pitfall record: import DPlayer from 'dplayer' is not allowed, it will cause build error: referenceerror: self is not defined -->
|
||||||
|
<!-- dplayer is asynchronous and needs to be initialized using import("dplayer").then(({ default: DPlayer }) => { }); -->
|
||||||
|
<!-- https://github.com/u2sb/www.u2sb.com/blob/main/docs/OpenSw/vuepress-plugin-smplayer/dplayer.md -->
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
// Video URL
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// Subtitle URL
|
||||||
|
zm: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// Autoplay
|
||||||
|
autoplay: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// Loop
|
||||||
|
loop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dp: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
import('dplayer').then(({ default: DPlayer }) => {
|
||||||
|
this.dp = new DPlayer({
|
||||||
|
// Player container element
|
||||||
|
container: this.$refs.dplayer,
|
||||||
|
// Enable live mode
|
||||||
|
live: false,
|
||||||
|
// Video autoplay
|
||||||
|
autoplay: this.autoplay,
|
||||||
|
// Theme color
|
||||||
|
theme: 'var(--vp-c-brand-1)',
|
||||||
|
// Video loop
|
||||||
|
loop: this.loop,
|
||||||
|
// Language
|
||||||
|
lang: 'en',
|
||||||
|
// Enable screenshot, if enabled, video and video cover need to allow cross-origin
|
||||||
|
screenshot: false,
|
||||||
|
// Enable hotkeys, support fast forward, rewind, volume control, play/pause
|
||||||
|
hotkey: true,
|
||||||
|
// Enable AirPlay in Safari
|
||||||
|
airplay: true,
|
||||||
|
// Enable Chromecast
|
||||||
|
chromecast: false,
|
||||||
|
// Video preload, optional values: 'none', 'metadata', 'auto'
|
||||||
|
preload: 'auto',
|
||||||
|
// Default volume, please note that the player will remember user settings, and the default volume will be invalid after the user manually sets the volume
|
||||||
|
volume: 0.5,
|
||||||
|
// Optional playback speed, can be set to a custom array
|
||||||
|
playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2],
|
||||||
|
// Show a logo in the upper left corner, you can adjust its size and position through CSS
|
||||||
|
logo: '',
|
||||||
|
// Prevent automatic toggle of play/pause when clicking on the player
|
||||||
|
preventClickToggle: false,
|
||||||
|
// Video information
|
||||||
|
video: {
|
||||||
|
// Video link
|
||||||
|
url: this.url,
|
||||||
|
// Video cover
|
||||||
|
pic: '/images/dplayer.png',
|
||||||
|
// Optional values: 'auto', 'hls', 'flv', 'dash', 'webtorrent', 'normal' or other custom types
|
||||||
|
type: 'auto'
|
||||||
|
// Quality switching
|
||||||
|
// quality:'',
|
||||||
|
// Default quality
|
||||||
|
// defaultQuality:''
|
||||||
|
// Video thumbnail, can be generated using DPlayer-thumbnails
|
||||||
|
// thumbnails:''
|
||||||
|
// Custom type
|
||||||
|
// customType:''
|
||||||
|
},
|
||||||
|
// Subtitle information
|
||||||
|
subtitle: {
|
||||||
|
// Subtitle link
|
||||||
|
url: this.zm,
|
||||||
|
// Subtitle type, optional values: 'webvtt', 'ass', currently only supports webvtt
|
||||||
|
type: 'webvtt',
|
||||||
|
// Subtitle font size
|
||||||
|
fontSize: '20px',
|
||||||
|
// Distance from the bottom of the player to the subtitle, the value is like: '10px' '10%'
|
||||||
|
bottom: '40px',
|
||||||
|
// Subtitle color
|
||||||
|
color: 'var(--vp-c-brand)'
|
||||||
|
},
|
||||||
|
// Danmaku
|
||||||
|
// https://dplayer.diygod.dev/zh/guide.html#%E5%8F%82%E6%95%B0
|
||||||
|
// Mutual exclusion, prevent multiple players from playing at the same time, pause other players when the current player is playing
|
||||||
|
mutex: true
|
||||||
|
})
|
||||||
|
// Disable right-click to download video
|
||||||
|
this.$refs.dplayer.oncontextmenu = () => {
|
||||||
|
document.querySelector('.dplayer-menu').style.display = 'none'
|
||||||
|
document.querySelector('.dplayer-mask').style.display = 'none'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Modify loop display
|
||||||
|
document
|
||||||
|
.getElementsByClassName('dplayer-setting-item dplayer-setting-loop')[0]
|
||||||
|
.getElementsByClassName('dplayer-label')[0].innerText = 'Loop'
|
||||||
|
// Modify playback speed display
|
||||||
|
document
|
||||||
|
.getElementsByClassName('dplayer-setting-item dplayer-setting-speed')[0]
|
||||||
|
.getElementsByClassName('dplayer-label')[0].innerText = 'Playback Speed'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.dp.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
</script>
|
||||||
34
src/components/Footer.vue
Executable file
34
src/components/Footer.vue
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Returns localized GitHub notice string in Turkish/English according to current route.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getLocalizedNotice(): string {
|
||||||
|
if (this.$route.name?.includes('blog'))
|
||||||
|
return 'Made with ❤️ by @andatoshiki at @toshikidev proudly co-founded in the innovative Arizona State University.'
|
||||||
|
else
|
||||||
|
return 'Made with ❤️ by @andatoshiki at @toshikidev proudly co-founded in the innovative Arizona State University.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="margin bg-gray-100 text-sm w-full py-4 text-black/50 dark:(bg-white/5 text-white/30)">
|
||||||
|
<div class="responsive-screen">
|
||||||
|
<div class="space-y-4 text-center sm:(space-y-0 space-x-6 text-left)">
|
||||||
|
<SmartLink
|
||||||
|
href="https://github.com/andatoshiki/toshiki-home-nuxt"
|
||||||
|
class="text-center border-b border-transparent hover:border-black/10 dark:hover:border-white/10 transition-colors"
|
||||||
|
blank
|
||||||
|
>
|
||||||
|
{{ getLocalizedNotice }}
|
||||||
|
</SmartLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
54
src/components/GoTop.vue
Executable file
54
src/components/GoTop.vue
Executable file
@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
position: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Checks if the position is higher than a specific number and returns a boolean value.
|
||||||
|
* @returns {boolean} Higher than the given number.
|
||||||
|
*/
|
||||||
|
isActive(): boolean {
|
||||||
|
return this.position > 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('scroll', this.updatePosition)
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener('scroll', this.updatePosition)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Updates the Vue data when it's called.
|
||||||
|
*/
|
||||||
|
updatePosition() {
|
||||||
|
this.position = window.scrollY
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Scrolls window to top.
|
||||||
|
*/
|
||||||
|
goTop() {
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-show="isActive" class="right-6 bottom-4 z-50 fixed items-center md:flex md:space-x-2">
|
||||||
|
<Button rounded elevated @click.native="goTop">
|
||||||
|
<template #icon>
|
||||||
|
<IconChevron up class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<ColorSwitcher class="hidden md:block" />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
14
src/components/Icon/AcademicHat.vue
Executable file
14
src/components/Icon/AcademicHat.vue
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path fill="transparent" d="M12 14l9-5-9-5-9 5 9 5z" />
|
||||||
|
<path
|
||||||
|
fill="transparent"
|
||||||
|
d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
10
src/components/Icon/At.vue
Executable file
10
src/components/Icon/At.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
11
src/components/Icon/Branch.vue
Executable file
11
src/components/Icon/Branch.vue
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M7 4.5v10M17 9.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM7 19.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM17 9.5A7.5 7.5 0 019.5 17"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.667"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
530
src/components/Icon/Brand.vue
Executable file
530
src/components/Icon/Brand.vue
Executable file
File diff suppressed because one or more lines are too long
10
src/components/Icon/Calendar.vue
Executable file
10
src/components/Icon/Calendar.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
100
src/components/Icon/Channel.vue
Executable file
100
src/components/Icon/Channel.vue
Executable file
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Netflix -->
|
||||||
|
<svg v-if="isSame('Netflix')" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M5.398 0v.006c3.028 8.556 5.37 15.175 8.348 23.596 2.344.058 4.85.398 4.854.398-2.8-7.924-5.923-16.747-8.487-24zm8.489 0v9.63L18.6 22.951c-.043-7.86-.004-15.913.002-22.95zM5.398 1.05V24c1.873-.225 2.81-.312 4.715-.398v-9.22z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Disney+ -->
|
||||||
|
<svg v-else-if="isSame('Disney+')" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M20.383 27.7s-1.45.099-2.449.205c-1.27.132-3.657.528-5.034 1.002-.413.142-1.253.48-1.326.889-.076.424.197.755.501 1.098.176.199 1.17 1.12 1.45 1.343 1.169.94 3.54 2.388 5.287 3.092.6.239 1.592.58 1.592.58s-.074-2.719-.06-5.397c.007-1.413.039-2.812.039-2.812zm26.597 1.082c.083.73-.112 2.112-.146 2.294-.062.42-.391 1.389-.446 1.507-.265.588-.527 1.07-.805 1.552-.476.823-1.607 2.13-2.278 2.688-2.497 2.076-6.362 3.259-9.678 3.648-2.25.262-4.835.223-7.22-.201a58.38 58.38 0 01-2.04-.415s.003.47-.036.8a3.23 3.23 0 01-.203.676c-.173.345-.458.523-.872.6-.5.088-1.03.118-1.49-.072-.758-.306-1.03-.989-1.162-1.775-.107-.63-.22-1.723-.22-1.723s-.566-.259-1.039-.486a27.105 27.105 0 01-4.037-2.38 50.303 50.303 0 01-2.087-1.684c-.889-.812-1.688-1.62-2.296-2.656-.473-.81-.61-1.528-.25-2.386.496-1.196 2.278-2.096 3.497-2.609.895-.38 3.678-1.255 4.834-1.416.546-.076 1.393-.222 1.445-.254a.324.324 0 00.052-.046c.026-.036.071-1.22.062-1.653-.01-.425.328-3.221.437-3.813.056-.32.308-1.55.565-1.873.168-.219.465-.201.708-.058 1.326.793 1.728 3.545 1.827 4.945.059.852.088 2.135.088 2.135s1.521-.043 2.457-.017c.91.02 1.911.158 2.855.303 1.208.186 3.563.68 4.914 1.34 1.112.542 2.153 1.456 2.49 2.423.314.887.267 1.5-.21 2.301-.537.904-1.552 1.576-2.581 1.632-.307.017-1.46-.13-1.814-.395-.14-.105-.132-.295-.032-.424.038-.046.577-.321.895-.482.16-.084.291-.174.416-.283.264-.224.502-.47.476-.759-.037-.375-.45-.606-.842-.755-1.845-.705-5.527-1.29-7.307-1.392-.696-.039-1.687-.072-1.687-.072L24.43 37s.819.15 1.464.251c.37.054 1.94.19 2.357.2 3.175.08 6.72-.193 9.633-1.516 1.28-.58 2.453-1.3 3.342-2.277a6.45 6.45 0 001.622-4.907c-.178-2.017-1.653-4.41-2.831-5.869-3.113-3.851-8.448-7.02-13.142-8.877-4.793-1.895-9.53-2.986-14.615-3.168-1.311-.047-4.17.017-5.615.402-.207.056-.415.122-.606.164-.152.035-.39.132-.456.183-.036.028-.072.067-.072.067s.09.048.174.082c.153.064.798.103 1.131.162.298.054.609.204.732.415.117.2.131.357-.008.524-.328.383-1.56.319-2.103.236-.564-.086-1.266-.253-1.395-.725-.15-.556.125-1.102.422-1.606.596-1.01 1.45-1.534 2.701-1.862 1.777-.47 4.02-.8 5.698-.861 3.796-.138 7.39.5 11.069 1.575 2.105.613 4.862 1.64 6.88 2.576 1.448.671 3.73 1.907 5.011 2.714.404.257 2.77 1.93 3.137 2.223a38.43 38.43 0 012.495 2.163c1.405 1.34 3.152 3.392 4 5.022.205.39.363.774.627 1.226.09.155.478 1.081.543 1.35.063.264.157.653.17.669.019.142.199.938.185 1.245z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Amazon Prime -->
|
||||||
|
<svg v-else-if="isSame('Amazon Prime')" viewbox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||||
|
<g fill-rule="evenodd">
|
||||||
|
<path
|
||||||
|
d="M14.506 12.63c-1.758 1.296-4.305 1.986-6.498 1.986-3.081 0-5.845-1.137-7.94-3.03-.159-.148-.016-.351.181-.235 2.26 1.316 5.06 2.107 7.943 2.107 2.082-.01 4.141-.43 6.06-1.239.297-.126.547.195.254.41zm.731-.837c-.223-.287-1.485-.135-2.05-.069-.173.021-.199-.129-.044-.237 1.01-.706 2.653-.503 2.846-.265.192.237-.05 1.89-.994 2.68-.145.12-.283.056-.212-.105.212-.529.687-1.716.462-2.004M9.585 6.75c0 .579.015 1.062-.291 1.577-.251.417-.645.675-1.077.675-.598 0-.948-.435-.948-1.077 0-1.267 1.189-1.498 2.316-1.498v.322zm1.57 3.625a.337.337 0 0 1-.368.035c-.516-.41-.61-.6-.893-.991-.854.832-1.46 1.081-2.565 1.081C6.018 10.5 5 9.728 5 8.182c0-1.207.683-2.03 1.66-2.431.844-.356 2.024-.418 2.925-.514V5.05c0-.354.03-.772-.19-1.078-.188-.274-.551-.387-.873-.387-.593 0-1.12.291-1.25.894-.026.137-.129.265-.27.272l-1.514-.162c-.126-.027-.268-.125-.231-.308C5.606 2.528 7.26 2 8.74 2c.758 0 1.747.193 2.344.74.758.676.685 1.578.685 2.56v2.318c0 .697.303 1.002.587 1.379.099.137.12.294-.006.395-.402.324-.8.654-1.193.987l-.002-.004"
|
||||||
|
class="dark:fill-white"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Apple TV+ -->
|
||||||
|
<svg
|
||||||
|
v-else-if="isSame('Apple TV+')"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke-width="0"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
height="1em"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M747.4 535.7c-.4-68.2 30.5-119.6 92.9-157.5-34.9-50-87.7-77.5-157.3-82.8-65.9-5.2-138 38.4-164.4 38.4-27.9 0-91.7-36.6-141.9-36.6C273.1 298.8 163 379.8 163 544.6c0 48.7 8.9 99 26.7 150.8 23.8 68.2 109.6 235.3 199.1 232.6 46.8-1.1 79.9-33.2 140.8-33.2 59.1 0 89.7 33.2 141.9 33.2 90.3-1.3 167.9-153.2 190.5-221.6-121.1-57.1-114.6-167.2-114.6-170.7zm-105.1-305c50.7-60.2 46.1-115 44.6-134.7-44.8 2.6-96.6 30.5-126.1 64.8-32.5 36.8-51.6 82.3-47.5 133.6 48.4 3.7 92.6-21.2 129-63.7z"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- FOX -->
|
||||||
|
<svg v-else-if="isSame('FOX')" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M5.901 2c-.005 0-.005.01-.005.018-.01 6.646-.01 13.333.003 19.977.007 0 .007.007.017.005h5.524c.003-.003.005-.006.005-.013 0-2.164-.01-4.287-.013-6.451 1.806-.013 3.636 0 5.452-.005v-5.456c-1.803-.002-3.621.01-5.411-.005.007-.841-.016-1.766.012-2.602h6.626c-.12-1.822-.24-3.644-.364-5.463C17.744 2 17.742 2 17.734 2H5.901z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Adult Swim -->
|
||||||
|
<svg v-else-if="isSame('Adult Swim')" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M22.318 19.124c.042-1.953.553-3.083 2.328-3.083 1.424 0 2.008.885 2.008 2.25 0 1.718-.353 2.046-2.28 2.587-1.776.501-4.14.916-5.722 1.84-1.575.917-2.602 3.124-2.602 5.766 0 4.872 2.091 6.88 5.678 6.88 2.134 0 4.141-1.168 5.01-3.173h.086c.024.879.11 1.8.346 2.587h6.148c-.743-.997-.743-3.176-.743-4.881V18.29c0-4.753-2.328-6.67-7.576-6.67-2.639 0-4.456.372-5.946 1.451-1.502 1.087-2.365 2.97-2.408 6.053h5.673zm-.115 8.688c0-1.259.316-1.97.985-2.5.59-.503 1.459-.547 3.466-1.677 0 .917-.03 2.095-.03 3.968 0 2.246-1.23 3.088-2.566 3.088-1.186 0-1.855-1.083-1.855-2.879zM2 6h9.977v4.675H7.91V38.8h4.067v4.669H2V6zM37.652 39.065h4.062V10.94h-4.062V6.266h9.966v37.468h-9.966v-4.67z"
|
||||||
|
class="dark:fill-white"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- BBC -->
|
||||||
|
<svg v-else-if="isSame('BBC')" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M31.12 24.43s4.274-1.853 4.236-6.77c0 0 .65-8.058-9.84-9.04H13.879V42.4h13.341s11.146.034 11.146-9.532c0 0 .263-6.509-7.245-8.438zm-5.09 12.75s6.244.303 6.244-4.804c0 0 .183-4.469-6.244-4.425h-6.244v9.229h6.244zm-1.485-23.303h-4.759v8.739h4.05s5.463-.076 5.463-4.73c0 0 .187-3.743-4.754-4.01z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- TNT -->
|
||||||
|
<svg v-else-if="isSame('TNT')" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M41 14.271H29.642v29.567h-9.926V14.27H8.344V5.732H41v8.54z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
platform: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: 'Netflix'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Checks if passed value matches with the prop value.
|
||||||
|
* @prop {string} value
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isSame(value: string): boolean {
|
||||||
|
return this.platform?.toLowerCase() === value?.toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
5
src/components/Icon/Check.vue
Executable file
5
src/components/Icon/Check.vue
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
84
src/components/Icon/Chevron.vue
Executable file
84
src/components/Icon/Chevron.vue
Executable file
@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Up -->
|
||||||
|
<svg v-if="up === true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Down -->
|
||||||
|
<svg
|
||||||
|
v-else-if="down === true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Right -->
|
||||||
|
<svg
|
||||||
|
v-else-if="right === true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Left -->
|
||||||
|
<svg
|
||||||
|
v-else-if="left === true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Double Left -->
|
||||||
|
<svg
|
||||||
|
v-else-if="doubleLeft === true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
up: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
down: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
doubleLeft: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
10
src/components/Icon/Clock.vue
Executable file
10
src/components/Icon/Clock.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
11
src/components/Icon/Cog.vue
Executable file
11
src/components/Icon/Cog.vue
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||||
|
/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
522
src/components/Icon/Dev.vue
Executable file
522
src/components/Icon/Dev.vue
Executable file
File diff suppressed because one or more lines are too long
9
src/components/Icon/Document.vue
Executable file
9
src/components/Icon/Document.vue
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
9
src/components/Icon/Dollar.vue
Executable file
9
src/components/Icon/Dollar.vue
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
9
src/components/Icon/Ellipsis.vue
Executable file
9
src/components/Icon/Ellipsis.vue
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
10
src/components/Icon/Exclamation.vue
Executable file
10
src/components/Icon/Exclamation.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
10
src/components/Icon/Eye.vue
Executable file
10
src/components/Icon/Eye.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
|
||||||
|
/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
9
src/components/Icon/Fire.vue
Executable file
9
src/components/Icon/Fire.vue
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12.395 2.553a1 1 0 00-1.45-.385c-.345.23-.614.558-.822.88-.214.33-.403.713-.57 1.116-.334.804-.614 1.768-.84 2.734a31.365 31.365 0 00-.613 3.58 2.64 2.64 0 01-.945-1.067c-.328-.68-.398-1.534-.398-2.654A1 1 0 005.05 6.05 6.981 6.981 0 003 11a7 7 0 1011.95-4.95c-.592-.591-.98-.985-1.348-1.467-.363-.476-.724-1.063-1.207-2.03zM12.12 15.12A3 3 0 017 13s.879.5 2.5.5c0-1 .5-4 1.25-4.5.5 1 .786 1.293 1.371 1.879A2.99 2.99 0 0113 13a2.99 2.99 0 01-.879 2.121z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
10
src/components/Icon/Fork.vue
Executable file
10
src/components/Icon/Fork.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M7.833 12h6.25a2.917 2.917 0 002.892-2.533 2.916 2.916 0 11.838.006 3.75 3.75 0 01-3.73 3.36h-6.25v1.696a2.917 2.917 0 11-.833 0V9.471a2.917 2.917 0 11.833 0V12zm-2.5 5.417a2.083 2.083 0 104.167 0 2.083 2.083 0 00-4.167 0v0zm0-10.834a2.083 2.083 0 104.167 0 2.083 2.083 0 00-4.167 0v0zM17.417 4.5a2.083 2.083 0 100 4.167 2.083 2.083 0 000-4.167z"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width=".833"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
10
src/components/Icon/Home.vue
Executable file
10
src/components/Icon/Home.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
5
src/components/Icon/IconBack.vue
Executable file
5
src/components/Icon/IconBack.vue
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
10
src/components/Icon/Inbox.vue
Executable file
10
src/components/Icon/Inbox.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8 4H6a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-2m-4-1v8m0 0l3-3m-3 3L9 8m-5 5h2.586a1 1 0 01.707.293l2.414 2.414a1 1 0 00.707.293h3.172a1 1 0 00.707-.293l2.414-2.414a1 1 0 01.707-.293H20"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
10
src/components/Icon/Link.vue
Executable file
10
src/components/Icon/Link.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
5
src/components/Icon/Menu.vue
Executable file
5
src/components/Icon/Menu.vue
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
10
src/components/Icon/Moon.vue
Executable file
10
src/components/Icon/Moon.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
8
src/components/Icon/MusicNote.vue
Executable file
8
src/components/Icon/MusicNote.vue
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M10 21q-1.65 0-2.825-1.175Q6 18.65 6 17q0-1.65 1.175-2.825Q8.35 13 10 13q.575 0 1.062.137q.488.138.938.413V3h6v4h-4v10q0 1.65-1.175 2.825Q11.65 21 10 21Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
11
src/components/Icon/Play.vue
Executable file
11
src/components/Icon/Play.vue
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M17.886 9.874L9.89 4.429a2.46 2.46 0 00-2.57-.126c-.4.219-.734.544-.966.942A2.594 2.594 0 006 6.559v10.887c0 .462.123.916.356 1.313.232.396.566.72.965.939a2.46 2.46 0 002.569-.127l7.996-5.445c.343-.233.624-.55.818-.92a2.597 2.597 0 000-2.41 2.536 2.536 0 00-.818-.92v-.002 0z"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
5
src/components/Icon/Plus.vue
Executable file
5
src/components/Icon/Plus.vue
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
10
src/components/Icon/Question.vue
Executable file
10
src/components/Icon/Question.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
10
src/components/Icon/Search.vue
Executable file
10
src/components/Icon/Search.vue
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
36
src/components/Icon/Star.vue
Executable file
36
src/components/Icon/Star.vue
Executable file
@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
v-if="filled === false"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
filled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user