init: initial commit for all project root sources

This commit is contained in:
Anda Toshiki 2024-05-09 00:51:12 -07:00
commit 324edea85e
187 changed files with 30192 additions and 0 deletions

13
.editorconfig Executable file
View 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
View 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
View 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
View File

@ -0,0 +1 @@
16

2
.npmrc Executable file
View File

@ -0,0 +1,2 @@
shamefully-hoist = true
auto-install-peers = true

15
.prettierignore Executable file
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

21
LICENSE Executable file
View 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
View 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
View File

@ -0,0 +1 @@
export default ['~/components']

14
config/constants.ts Executable file
View 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
View File

@ -0,0 +1 @@
export default ['@/stylesheets/root']

7
config/generate.ts Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
}
}

View 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
View File

@ -0,0 +1,5 @@
export default {
experimentWarning: false,
build: false,
ssr: false
}

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"~/*": ["./*"],
"@/*": ["./src/*"],
"~~/*": ["./*"],
"@@/*": ["./src/*"]
}
},
"exclude": ["node_modules", ".nuxt", "dist"]
}

5
netlify.toml Executable file
View File

@ -0,0 +1,5 @@
[[redirects]]
force = true
from = "/blog/gonderi/:slug"
status = 301
to = "/blog/:slug"

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

48
prettier.config.js Executable file
View 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
View 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
}

View 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
}[]
}

View 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

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,5 @@
<template>
<div class="flex flex-col gap-2 md:(grid grid-flow-col auto-cols-fr)">
<slot />
</div>
</template>

View 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>

View 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
View 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>

View 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>

View 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
View 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>

View 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
View 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
View 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>

View 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
View 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
View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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
View 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>

View 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
View 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
View 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
View 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>

View 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
View 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
View 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

File diff suppressed because one or more lines are too long

View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

View 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
View 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>

View 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>

View 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
View 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
View 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
View 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
View 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>

View 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
View 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
View 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
View 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
View 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>

View 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
View 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
View 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>

View 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
View 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
View 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