feat: support pwa
15
.gitignore
vendored
@ -41,8 +41,17 @@ yarn-error.log*
|
||||
# Prevent commiting lock file.
|
||||
package-lock.json
|
||||
|
||||
secret.json
|
||||
|
||||
# sub module
|
||||
api-docs/
|
||||
|
||||
api-docs/
|
||||
# next-pwa
|
||||
public/precache.*.*.js
|
||||
public/sw.js
|
||||
public/workbox-*.js
|
||||
public/worker-*.js
|
||||
public/fallback-*.js
|
||||
public/precache.*.*.js.map
|
||||
public/sw.js.map
|
||||
public/workbox-*.js.map
|
||||
public/worker-*.js.map
|
||||
public/fallback-*.js
|
||||
@ -11,7 +11,7 @@ import { User, UserCache } from '@types'
|
||||
import DiscordAvatar from '@components/DiscordAvatar'
|
||||
import Fetch from '@utils/Fetch'
|
||||
|
||||
const Navbar = ({ token }:{ token: string }): JSX.Element => {
|
||||
const Navbar = ({ token, pwa }:{ token: string, pwa: boolean }): JSX.Element => {
|
||||
const [userCache, setUserCache] = useState<UserCache>()
|
||||
const [navbarOpen, setNavbarOpen] = useState<boolean>(false)
|
||||
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false)
|
||||
@ -124,18 +124,11 @@ const Navbar = ({ token }:{ token: string }): JSX.Element => {
|
||||
</ul>
|
||||
</div>
|
||||
</> :
|
||||
<a tabIndex={0} onKeyPress={()=> {
|
||||
if(!(logged)) {
|
||||
localStorage.redirectTo = window.location.href
|
||||
setNavbarOpen(false)
|
||||
redirectTo(router, 'login')
|
||||
}
|
||||
}} onClick={()=> {
|
||||
if(!(logged)) {
|
||||
localStorage.redirectTo = window.location.href
|
||||
setNavbarOpen(false)
|
||||
redirectTo(router, 'login')
|
||||
}
|
||||
<a tabIndex={0} onClick={()=> {
|
||||
localStorage.redirectTo = window.location.href
|
||||
setNavbarOpen(false)
|
||||
if(pwa) window.open('/api/auth/discord')
|
||||
else redirectTo(router, 'login')
|
||||
}} className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100 cursor-pointer outline-none'>
|
||||
로그인
|
||||
</a>
|
||||
@ -204,15 +197,15 @@ const Navbar = ({ token }:{ token: string }): JSX.Element => {
|
||||
<i className='fas fa-sign-out-alt' />
|
||||
<span className='px-2 font-medium'>로그아웃</span>
|
||||
</a>
|
||||
</> : <Link href='/api/auth/discord'>
|
||||
<a onClick={()=> {
|
||||
localStorage.redirectTo = window.location.href
|
||||
setNavbarOpen(false)
|
||||
}} className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
||||
<i className='far fa-user' />
|
||||
<span className='px-2 font-medium'>로그인</span>
|
||||
</a>
|
||||
</Link>
|
||||
</> : <a onClick={() => {
|
||||
localStorage.redirectTo = window.location.href
|
||||
setNavbarOpen(false)
|
||||
if(pwa) window.open('/api/auth/discord')
|
||||
else redirectTo(router, 'login')
|
||||
}} className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
||||
<i className='far fa-user' />
|
||||
<span className='px-2 font-medium'>로그인</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { withSentryConfig } = require('@sentry/nextjs')
|
||||
const withPWA = require('next-pwa')
|
||||
const VERSION = require('./package.json').version
|
||||
|
||||
module.exports = withSentryConfig({
|
||||
module.exports = withSentryConfig(withPWA({
|
||||
pwa: {
|
||||
dest: 'public'
|
||||
},
|
||||
env: {
|
||||
NEXT_PUBLIC_RELEASE_VERSION: VERSION,
|
||||
SENTRY_SKIP_AUTO_RELEASE: true
|
||||
@ -13,4 +17,4 @@ module.exports = withSentryConfig({
|
||||
experimental: {
|
||||
scrollRestoration: true
|
||||
}
|
||||
}, {})
|
||||
}), {})
|
||||
@ -40,6 +40,7 @@
|
||||
"mysql": "2.18.1",
|
||||
"next": "10.2.0",
|
||||
"next-connect": "0.10.1",
|
||||
"next-pwa": "5.2.21",
|
||||
"next-session": "3.4.0",
|
||||
"node-emoji": "1.10.0",
|
||||
"nprogress": "0.2.0",
|
||||
|
||||
@ -7,7 +7,7 @@ import { GlobalHotKeys } from 'react-hotkeys'
|
||||
import NProgress from 'nprogress'
|
||||
|
||||
import Logger from '@utils/Logger'
|
||||
import { parseCookie, systemTheme } from '@utils/Tools'
|
||||
import { handlePWA, parseCookie, systemTheme } from '@utils/Tools'
|
||||
import { shortcutKeyMap } from '@utils/Constants'
|
||||
import { Theme } from '@types'
|
||||
|
||||
@ -34,6 +34,7 @@ Router.events.on('routeChangeError', NProgress.done)
|
||||
const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps): JSX.Element => {
|
||||
const [ shortcutModal, setShortcutModal ] = useState(false)
|
||||
const [ theme, setTheme ] = useState<Theme>('system')
|
||||
const [ standalone, setStandalone ] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
@ -50,24 +51,17 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
|
||||
setTheme(systemTheme())
|
||||
}
|
||||
else setTheme(localStorage.theme)
|
||||
setStandalone(handlePWA())
|
||||
}, [])
|
||||
|
||||
return <div className={theme}>
|
||||
<Head>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' />
|
||||
<title>한국 디스코드봇 리스트</title>
|
||||
<meta name='description' content='다양한 국내 디스코드봇들을 확인하고, 초대해보세요!' />
|
||||
<meta name='og:title' content='한국 디스코드봇 리스트' />
|
||||
<meta name='og:url' content='https://koreanbots.dev' />
|
||||
<meta name='og:description' content='다양한 국내 디스코드봇들을 확인하고, 초대해보세요!' />
|
||||
<meta name='og:image' content='/logo.png' />
|
||||
<meta charSet='utf-8' />
|
||||
<link rel='shortcut icon' href='/logo.png' />
|
||||
<meta name='theme-color' content='#3366FF' />
|
||||
</Head>
|
||||
<Navbar token={cookie.token} />
|
||||
<Navbar token={cookie.token} pwa={standalone} />
|
||||
<div className='iu-is-the-best min-h-screen text-black dark:text-gray-100 dark:bg-discord-dark bg-white'>
|
||||
<Component {...pageProps} err={err} theme={theme} setTheme={setTheme} />
|
||||
<Component {...pageProps} err={err} theme={theme} setTheme={setTheme} pwa={standalone} />
|
||||
</div>
|
||||
{
|
||||
!(router.pathname.startsWith('/developers')) && <Footer theme={theme} setTheme={setTheme} />
|
||||
|
||||
@ -10,11 +10,36 @@ class MyDocument extends Document {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<meta charSet='utf-8' />
|
||||
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
|
||||
<meta name='description' content='다양한 국내 디스코드봇들을 확인하고, 초대해보세요!' />
|
||||
<meta name='og:title' content='한국 디스코드봇 리스트' />
|
||||
<meta name='og:url' content='https://koreanbots.dev' />
|
||||
<meta name='og:description' content='다양한 국내 디스코드봇들을 확인하고, 초대해보세요!' />
|
||||
<meta name='og:image' content='/favicon.ico' />
|
||||
<link rel='shortcut icon' href='/favicon.ico' />
|
||||
<link
|
||||
rel='stylesheet'
|
||||
href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.6.0/styles/solarized-dark.min.css'
|
||||
/>
|
||||
<link rel='search' type='application/opensearchdescription+xml' title='한국 디스코드봇 리스트' href='/opensearch.xml' />
|
||||
<link rel='apple-touch-icon' sizes='57x57' href='/apple-icon-57x57.png' />
|
||||
<link rel='apple-touch-icon' sizes='60x60' href='/apple-icon-60x60.png' />
|
||||
<link rel='apple-touch-icon' sizes='72x72' href='/apple-icon-72x72.png' />
|
||||
<link rel='apple-touch-icon' sizes='76x76' href='/apple-icon-76x76.png' />
|
||||
<link rel='apple-touch-icon' sizes='114x114' href='/apple-icon-114x114.png' />
|
||||
<link rel='apple-touch-icon' sizes='120x120' href='/apple-icon-120x120.png' />
|
||||
<link rel='apple-touch-icon' sizes='144x144' href='/apple-icon-144x144.png' />
|
||||
<link rel='apple-touch-icon' sizes='152x152' href='/apple-icon-152x152.png' />
|
||||
<link rel='apple-touch-icon' sizes='180x180' href='/apple-icon-180x180.png' />
|
||||
<link rel='icon' type='image/png' sizes='192x192' href='/android-icon-192x192.png' />
|
||||
<link rel='icon' type='image/png' sizes='32x32' href='/favicon-32x32.png' />
|
||||
<link rel='icon' type='image/png' sizes='96x96' href='/favicon-96x96.png' />
|
||||
<link rel='icon' type='image/png' sizes='16x16' href='/favicon-16x16.png' />
|
||||
<link rel='manifest' href='/manifest.json' />
|
||||
<meta name='msapplication-TileColor' content='#3366FF' />
|
||||
<meta name='msapplication-TileImage' content='/ms-icon-144x144.png' />
|
||||
<meta name='theme-color' content='#3366FF' />
|
||||
<script src='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js'></script>
|
||||
<script
|
||||
data-ad-client='ca-pub-4856582423981759'
|
||||
|
||||
29
pages/_offline.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { NextPage } from 'next'
|
||||
import Link from 'next/link'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
|
||||
const MyError: NextPage = () => {
|
||||
return <div
|
||||
className='flex items-center h-screen select-none px-20'
|
||||
>
|
||||
<Container>
|
||||
<h2 className='text-4xl font-semibold'>인터넷이 끊어졌나봐요...</h2>
|
||||
<p className='text-md mt-1'>인터넷 연결을 확인하시고 다시 시도 해주세요!</p>
|
||||
<a className='text-sm text-blue-500 hover:text-blue-400' href='https://status.koreanbots.dev' target='_blank' rel='noreferrer'>상태 페이지</a>
|
||||
<div>
|
||||
<Link href='/discord'>
|
||||
<a target='_blank' rel='noreferrer' className='text-lg hover:opacity-80 cursor-pointer'>
|
||||
<i className='fab fa-discord' />
|
||||
</a>
|
||||
</Link>
|
||||
<a href='https://twitter.com/koreanbots' target='_blank' rel='noreferrer' className='text-lg ml-2 hover:opacity-80 cursor-pointer'>
|
||||
<i className='fab fa-twitter' />
|
||||
</a>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default MyError
|
||||
@ -1,7 +1,7 @@
|
||||
import { NextPage} from 'next'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { redirectTo } from '@utils/Tools'
|
||||
|
||||
@ -9,13 +9,19 @@ const Loader = dynamic(() => import('@components/Loader'))
|
||||
|
||||
const DiscordCallback:NextPage = () => {
|
||||
const router = useRouter()
|
||||
const [ notRedirecting, setNotRedirecting ] = useState(false)
|
||||
useEffect(() => {
|
||||
redirectTo(router, localStorage.redirectTo ?? '/')
|
||||
localStorage.removeItem('redirectTo')
|
||||
if(window.opener) {
|
||||
setNotRedirecting(true)
|
||||
window.opener.location.reload()
|
||||
} else {
|
||||
redirectTo(router, localStorage.redirectTo ?? '/')
|
||||
localStorage.removeItem('redirectTo')
|
||||
}
|
||||
}, [router])
|
||||
|
||||
return <>
|
||||
<Loader text={<>리다이렉트 중 입니다.</>} />
|
||||
<Loader text={notRedirecting ? '해당 창을 닫고 원래 앱으로 돌아가주세요.' : '리다이렉트 중 입니다.'} />
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
BIN
public/android-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/android-icon-192x192.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/android-icon-36x36.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/android-icon-48x48.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/android-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/android-icon-96x96.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/apple-icon-114x114.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/apple-icon-120x120.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/apple-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/apple-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
public/apple-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
public/apple-icon-57x57.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/apple-icon-60x60.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/apple-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/apple-icon-76x76.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/apple-icon-precomposed.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/apple-icon.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
2
public/browserconfig.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
||||
BIN
public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
47
public/manifest.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "한국 디스코드봇 리스트",
|
||||
"short_name": "한국 디스코드봇 리스트",
|
||||
"lang": "ko-KR",
|
||||
"description": "다양한 국내 디스코드봇들을 확인하고, 초대해보세요!",
|
||||
"start_url": "/",
|
||||
"theme_color": "#3366FF",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "\/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image\/png",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image\/png",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image\/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image\/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image\/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image\/png",
|
||||
"density": "4.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
public/ms-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/ms-icon-150x150.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
public/ms-icon-310x310.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/ms-icon-70x70.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
1
types/global.d.ts
vendored
@ -2,6 +2,7 @@ import * as Yup from 'yup'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ga(a: string, b: string, c: string): void
|
||||
hljs: {
|
||||
initHighlighting(): void
|
||||
highlightBlock(e: Element): void
|
||||
|
||||
@ -6,6 +6,16 @@ import { BotFlags, ImageOptions, UserFlags } from '@types'
|
||||
import { BASE_URLs, KoreanbotsEndPoints, Oauth } from './Constants'
|
||||
import { NextRouter } from 'next/router'
|
||||
|
||||
export function handlePWA(): boolean {
|
||||
let displayMode = 'browser'
|
||||
const mqStandAlone = '(display-mode: standalone)'
|
||||
if (window.navigator.standalone || window.matchMedia(mqStandAlone).matches) {
|
||||
displayMode = 'standalone'
|
||||
}
|
||||
window?.ga('set', 'dimension1', displayMode)
|
||||
|
||||
return displayMode === 'standalone'
|
||||
}
|
||||
export function formatNumber(value: number):string {
|
||||
const suffixes = ['', '만', '억', '조','해']
|
||||
const suffixNum = Math.floor((''+value).length/4)
|
||||
|
||||