mirror of
https://github.com/Aviortheking/next-template.git
synced 2025-07-24 22:29:51 +00:00
20
src/client/components/HelloWorld.tsx
Normal file
20
src/client/components/HelloWorld.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default class HelloWorld extends React.Component<Props> {
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<h1>{this.props.children}</h1>
|
||||
<style jsx>{`
|
||||
h1
|
||||
font-weight 700
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
209
src/client/components/Image.tsx
Normal file
209
src/client/components/Image.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
import React, { SyntheticEvent } from 'react'
|
||||
import css from '@smd/Image.module.styl'
|
||||
|
||||
interface Props {
|
||||
defaultHeight?: number
|
||||
src?: string
|
||||
sources?: Array<string>
|
||||
deleteOnError?: boolean
|
||||
downgradeOnError?: string
|
||||
canFullscreen?: boolean
|
||||
max?: {
|
||||
height?: number|string
|
||||
width?: number|string
|
||||
}
|
||||
width?: number|string
|
||||
default?: {
|
||||
height?: number|string
|
||||
width?: number|string
|
||||
}
|
||||
alt?: string
|
||||
}
|
||||
|
||||
const mimeTypes = {
|
||||
apng: 'image/apng',
|
||||
bmp: 'image/bmp',
|
||||
gif: 'image/gif',
|
||||
|
||||
ico: 'image/x-icon',
|
||||
cur: 'image/x-icon',
|
||||
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
jfif: 'image/jpeg',
|
||||
pjpeg: 'image/jpeg',
|
||||
pjp: 'image/jpeg',
|
||||
|
||||
png: 'image/png',
|
||||
svg: 'image/svg+xml',
|
||||
|
||||
tif: 'image/tiff',
|
||||
tiff: 'image/tiff',
|
||||
|
||||
webp: 'image/webp',
|
||||
}
|
||||
|
||||
const getMimeType = (img: string) => {
|
||||
const arr = img.split('.')
|
||||
return mimeTypes[arr[arr.length-1] as 'apng'] || mimeTypes.png
|
||||
}
|
||||
|
||||
type evType<T = HTMLImageElement> = SyntheticEvent<T, Event>
|
||||
|
||||
export default class Image extends React.Component<Props> {
|
||||
|
||||
private ref: React.RefObject<HTMLImageElement> = React.createRef()
|
||||
private plchldr: React.RefObject<HTMLDivElement> = React.createRef()
|
||||
private parent: React.RefObject<HTMLDivElement> = React.createRef()
|
||||
private pic: React.RefObject<HTMLPictureElement> = React.createRef()
|
||||
|
||||
private wasDowngraded = false
|
||||
private cardPos: Array<number> = []
|
||||
private cardSize: Array<number> = []
|
||||
|
||||
private isFullscreen = false
|
||||
|
||||
public async componentDidMount() {
|
||||
if (this.props.canFullscreen) {
|
||||
window.addEventListener('scroll', this.onScroll)
|
||||
window.addEventListener('resize', this.onResize)
|
||||
this.onScroll()
|
||||
this.onResize()
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidUpdate() {
|
||||
this.pic.current?.classList.remove(css.none)
|
||||
if (this.props.canFullscreen) {
|
||||
this.onScroll()
|
||||
this.onResize()
|
||||
}
|
||||
if (this.isFullscreen) {this.onClick()}
|
||||
}
|
||||
|
||||
public async componentWillUnmount() {
|
||||
if (this.props.canFullscreen) {
|
||||
window.removeEventListener('scroll', this.onScroll)
|
||||
window.removeEventListener('resize', this.onResize)
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const pic = (
|
||||
<picture ref={this.pic}>
|
||||
{this.props.sources && this.props.sources.map((el,index) => (
|
||||
<source key={index} srcSet={el} type={getMimeType(el)}/>
|
||||
))}
|
||||
<img
|
||||
className={css.image}
|
||||
ref={this.ref}
|
||||
src={this.props.src}
|
||||
onClick={this.props.canFullscreen && this.onClick || undefined}
|
||||
onLoad={this.props.default && this.onLoad || undefined}
|
||||
onError={this.props.deleteOnError && this.onError || undefined}
|
||||
style={{
|
||||
width: this.props.default?.width,
|
||||
height: this.props.default?.height,
|
||||
maxHeight: this.props.max?.height,
|
||||
maxWidth: this.props.max?.width,
|
||||
}}
|
||||
alt={this.props.alt}
|
||||
/>
|
||||
</picture>
|
||||
)
|
||||
if (this.props.canFullscreen) {return (
|
||||
<div ref={this.parent}>
|
||||
<div ref={this.plchldr} className={css.none}></div>
|
||||
{pic}
|
||||
</div>
|
||||
)}
|
||||
return pic
|
||||
}
|
||||
|
||||
private onScroll = async () => {
|
||||
if (!this.ref.current || this.isFullscreen || !this.props.canFullscreen) {
|
||||
return
|
||||
}
|
||||
|
||||
this.cardPos = [this.ref.current.offsetTop - window.scrollY, this.ref.current.offsetLeft - window.scrollX]
|
||||
this.ref.current.style.top = this.cardPos[0] + 'px'
|
||||
this.ref.current.style.left = this.cardPos[1] + 'px'
|
||||
}
|
||||
|
||||
private onResize = async () => {
|
||||
if (!this.ref.current || !this.plchldr.current || !this.props.canFullscreen || this.isFullscreen) {
|
||||
return
|
||||
}
|
||||
let tmp = [this.ref.current.offsetHeight, this.ref.current.offsetWidth]
|
||||
if (this.parent.current) {
|
||||
tmp = [this.parent.current.offsetHeight, this.ref.current.offsetWidth]
|
||||
}
|
||||
this.plchldr.current.style.width = `${tmp[1]}px`
|
||||
this.plchldr.current.style.height = `${tmp[0]}px`
|
||||
}
|
||||
|
||||
private onClick = async () => {
|
||||
if (!this.ref.current || !this.props.canFullscreen || !this.plchldr.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const i = this.ref.current
|
||||
const c = this.plchldr.current
|
||||
const body = document.body
|
||||
i.style.top = this.cardPos[0] + 'px'
|
||||
i.style.left = this.cardPos[1] + 'px'
|
||||
|
||||
if (this.isFullscreen) {
|
||||
i.style.width = this.cardSize[1] + 'px'
|
||||
i.style.height = this.cardSize[0] + 'px'
|
||||
body.classList.remove(css.hideOverflow)
|
||||
i.classList.remove(css.ph2)
|
||||
i.classList.add(css.after)
|
||||
|
||||
setTimeout(() => {
|
||||
if (i.classList.contains(css.ph2) || i.classList.contains(css.ph1) || this.isFullscreen) {return}
|
||||
const w = this.valToPixel(this.props.width)
|
||||
const mh = this.valToPixel(this.props.max?.height)
|
||||
const mw = this.valToPixel(this.props.max?.width)
|
||||
c.classList.add(css.none)
|
||||
i.style.height = ''
|
||||
i.style.width = w
|
||||
i.style.maxHeight = mh
|
||||
i.style.maxWidth = mw
|
||||
i.classList.remove(css.after)
|
||||
}, 350)
|
||||
this.isFullscreen = false
|
||||
} else {
|
||||
i.classList.add(css.ph1)
|
||||
c.classList.remove(css.none)
|
||||
i.classList.add(css.ph2)
|
||||
i.classList.remove(css.ph1)
|
||||
body.classList.add(css.hideOverflow)
|
||||
this.isFullscreen = true
|
||||
}
|
||||
}
|
||||
|
||||
private valToPixel(value: number|string|undefined): string {
|
||||
if (typeof value === 'number') {
|
||||
return `${value}px`
|
||||
}
|
||||
if (typeof value === 'undefined') {
|
||||
return ''
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private onLoad = async (ev: evType) => {
|
||||
ev.currentTarget.style.height = ''
|
||||
ev.currentTarget.style.width = ''
|
||||
}
|
||||
|
||||
private onError = async (ev: evType) => {
|
||||
this.w('Picture not loaded', ev.currentTarget.src)
|
||||
ev.currentTarget.parentElement?.classList.add(css.none)
|
||||
}
|
||||
|
||||
private w(...messages: any) {
|
||||
console.warn('[ Picture ]', ...messages)
|
||||
}
|
||||
}
|
0
src/client/libs/.gitkeep
Normal file
0
src/client/libs/.gitkeep
Normal file
2
src/client/styl/index.styl
Normal file
2
src/client/styl/index.styl
Normal file
@ -0,0 +1,2 @@
|
||||
h1
|
||||
font-weight 600
|
0
src/client/styl/modules/.gitkeep
Normal file
0
src/client/styl/modules/.gitkeep
Normal file
41
src/client/styl/modules/Image.module.styl
Normal file
41
src/client/styl/modules/Image.module.styl
Normal file
@ -0,0 +1,41 @@
|
||||
.image
|
||||
transition .3s
|
||||
object-fit contain
|
||||
z-index 2
|
||||
transition .3s
|
||||
|
||||
|
||||
&.ph1
|
||||
position fixed
|
||||
transition 0s
|
||||
|
||||
|
||||
&.ph2
|
||||
width 100% !important
|
||||
position fixed
|
||||
max-width 100% !important
|
||||
max-height 100% !important
|
||||
box-sizing border-box
|
||||
padding 5%
|
||||
top 0 !important
|
||||
left 0 !important
|
||||
height 100%
|
||||
background #000000A0
|
||||
|
||||
|
||||
&.after
|
||||
background #00000000
|
||||
/* height 100% */
|
||||
box-sizing border-box
|
||||
position fixed
|
||||
/* width 100% */
|
||||
z-index 0
|
||||
padding initial
|
||||
|
||||
|
||||
.hideOverflow
|
||||
overflow hidden
|
||||
|
||||
|
||||
.none
|
||||
display none
|
4
src/client/styl/stylus.d.ts
vendored
Normal file
4
src/client/styl/stylus.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module '*.styl' {
|
||||
const content: any
|
||||
export = content
|
||||
}
|
0
src/common/libs/.gitkeep
Normal file
0
src/common/libs/.gitkeep
Normal file
11
src/pages/404.tsx
Normal file
11
src/pages/404.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import Error from './_error'
|
||||
|
||||
export default class E404 extends React.Component {
|
||||
|
||||
public render() {
|
||||
return(
|
||||
<Error statusCode={404} />
|
||||
)
|
||||
}
|
||||
}
|
50
src/pages/_app.tsx
Normal file
50
src/pages/_app.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import App from 'next/app'
|
||||
import Head from 'next/head'
|
||||
import React from 'react'
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import ErrorPage from './_error'
|
||||
|
||||
import '@styl/index.styl'
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
})
|
||||
|
||||
interface States {
|
||||
errorId?: string
|
||||
}
|
||||
|
||||
export default class CApp extends App<Record<string, unknown>, Record<string, unknown>, States> {
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.log('catching error', error, errorInfo)
|
||||
Sentry.withScope((scope) => {
|
||||
Object.keys(errorInfo).forEach((key) => {
|
||||
scope.setExtra(key, (errorInfo as any)[key])
|
||||
})
|
||||
const id = Sentry.captureException(error)
|
||||
console.log(id)
|
||||
this.setState({
|
||||
errorId: id
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { Component, pageProps } = this.props
|
||||
|
||||
return(
|
||||
<>
|
||||
<Head>
|
||||
<title>Next Template Hello World !</title>
|
||||
</Head>
|
||||
{this.state && this.state.errorId && (
|
||||
<ErrorPage statusCode={500} eventId={this.state.errorId} />
|
||||
) || (
|
||||
<Component {...pageProps} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
25
src/pages/_document.tsx
Normal file
25
src/pages/_document.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||
import * as Sentry from '@sentry/browser'
|
||||
|
||||
process.on('unhandledRejection', (err) => {
|
||||
Sentry.captureException(err)
|
||||
})
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
Sentry.captureException(err)
|
||||
})
|
||||
|
||||
export default class CDocument extends Document {
|
||||
public render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
31
src/pages/_error.tsx
Normal file
31
src/pages/_error.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
|
||||
import { NextPageContext } from 'next'
|
||||
import * as Sentry from '@sentry/browser'
|
||||
|
||||
interface Props {
|
||||
statusCode: number
|
||||
eventId?: string
|
||||
}
|
||||
|
||||
export default class Error extends React.Component<Props> {
|
||||
|
||||
public static async getInitialProps({res, err}: NextPageContext): Promise<Props> {
|
||||
const statusCode = res?.statusCode || err?.statusCode || 500
|
||||
return {
|
||||
statusCode
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public render() {
|
||||
return(
|
||||
<>
|
||||
<h1>An Error Occured ! {this.props.statusCode}</h1>
|
||||
{this.props.eventId && (
|
||||
<button onClick={() => Sentry.showReportDialog({ eventId: this.props.eventId })}>Report Error</button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
34
src/pages/index.tsx
Normal file
34
src/pages/index.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import HelloWorld from '@cp/HelloWorld'
|
||||
import Image from '@cp/Image'
|
||||
|
||||
function isWindow() {
|
||||
try {
|
||||
window.isNaN(1)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export default class Index extends React.Component {
|
||||
|
||||
public render() {
|
||||
if (isWindow()) {
|
||||
throw new Error('Test')
|
||||
}
|
||||
|
||||
return(
|
||||
<>
|
||||
<HelloWorld>Hello World</HelloWorld>
|
||||
<Image
|
||||
max={{height:400,width:200}}
|
||||
default={{width:200,height:100}}
|
||||
deleteOnError={true}
|
||||
src="https://source.unsplash.com/random/800x600"
|
||||
/>
|
||||
<HelloWorld>Hello World</HelloWorld>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
22
src/server/server.ts
Normal file
22
src/server/server.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// Base Server Config
|
||||
// https://nextjs.org/docs/advanced-features/custom-server
|
||||
import { createServer } from 'http'
|
||||
import { parse } from 'url'
|
||||
import next from 'next'
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production'
|
||||
const app = next({ dev })
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
;(async () => {
|
||||
await app.prepare()
|
||||
createServer((req, res) => {
|
||||
// Be sure to pass `true` as the second argument to `url.parse`.
|
||||
// This tells it to parse the query portion of the URL.
|
||||
const parsedUrl = parse(req.url || '', true)
|
||||
|
||||
handle(req, res, parsedUrl)
|
||||
}).listen(parseInt(process.env.PORT || '3000', 10), () => {
|
||||
console.log('> Ready on http://localhost:3000')
|
||||
})
|
||||
})()
|
Reference in New Issue
Block a user