diff --git a/packages/status/public/locales/en/common.json b/packages/status/public/locales/en/common.json index 6d00725f..0dfa69c9 100644 --- a/packages/status/public/locales/en/common.json +++ b/packages/status/public/locales/en/common.json @@ -1,15 +1,29 @@ { + "due_to": "{{service}} is down due to {{cause}}", + "downtime": "Downtime", + "home": "Home", + "index": "Index", + "history_events": "History Events", + "service_monitor": "Service Monitor", + "normal": "Normal", + "days_ago": "days ago", + "today": "Today", + "copied": "Copied", + "Explorer": "Explorer", + "Tip Block Time": "Tip Block Time", + "Block Height": "Block Height", + "RPC&API Status": "RPC&API Status", + "About Neuron": "About Neuron", + "Advanced Features": "Advanced Features", "All services are online": "All services are online", "Announcement": "Announcement", "Assets": "Assets", "Axon Explorer": "Axon Explorer", - "Backup wallet": "Backup wallet", "Beginner's Guide": "Beginner's Guide", "Change log": "Change log", "Changelog": "Changelog", "CKB Explorer": "CKB Explorer", "Copyright © 2023 Magickbase All Rights Reserved.": "Copyright © 2023 Magickbase All Rights Reserved.", - "Create wallet": "Create wallet", "Develop guide": "Develop guide", "Download Neuron": "Download Neuron", "Foundation": "Foundation", @@ -20,14 +34,17 @@ "Kuai": "Kuai", "Language": "Language", "Nervos": "Nervos", + "Neuron": "Neuron", "Neuron Wallet": "Neuron Wallet", "Public Node": "Public Node", "Report": "Report", "Safety": "Safety", + "Service Monitor": "Service Monitor", "Services": "Services", "Services status loading...": "Services status loading...", "Some services are offline": "Some services are offline", + "Status": "Status", "Sync": "Sync", "Transaction": "Transaction", - "Transfer and receive": "Transfer and receive" + "Usage Tutorial": "Usage Tutorial" } diff --git a/packages/status/public/locales/zh/common.json b/packages/status/public/locales/zh/common.json index d52171fa..d51230bd 100644 --- a/packages/status/public/locales/zh/common.json +++ b/packages/status/public/locales/zh/common.json @@ -1,15 +1,29 @@ { + "due_to": "{{service}} 由于 {{cause}} 而停机", + "downtime": "停机", + "home": "主页", + "index": "指标", + "history_events": "历史事件", + "service_monitor": "服务监控", + "normal": "正常", + "days_ago": "天前", + "today": "今天", + "copied": "已复制", + "Explorer": "浏览器", + "Tip Block Time": "提示区块时间", + "Block Height": "区块高度", + "RPC&API Status": "RPC&API 状态", + "About Neuron": "关于 Neuron", + "Advanced Features": "高级功能", "All services are online": "所有服务均可用", "Announcement": "公告", - "Assets": "Assets", + "Assets": "资产", "Axon Explorer": "Axon 区块浏览器", - "Backup wallet": "备份钱包", "Beginner's Guide": "新手指南", "Change log": "更新日志", "Changelog": "更新日志", "CKB Explorer": "CKB 区块浏览器", "Copyright © 2023 Magickbase All Rights Reserved.": "Copyright © 2023 Magickbase 版权所有", - "Create wallet": "创建钱包", "Develop guide": "开发指南", "Download Neuron": "下载 Neuron", "Foundation": "基金会", @@ -20,14 +34,17 @@ "Kuai": "Kuai", "Language": "语言", "Nervos": "Nervos", + "Neuron": "Neuron", "Neuron Wallet": "Neuron 钱包", "Public Node": "公共节点", - "Report": "Report", - "Safety": "Safety", + "Report": "报告", + "Safety": "安全", + "Service Monitor": "服务监控", "Services": "服务", - "Services status loading...": "Services status loading...", - "Some services are offline": "Some services are offline", + "Services status loading...": "服务状态加载中...", + "Some services are offline": "部分服务不可用", + "Status": "状态", "Sync": "同步", "Transaction": "交易", - "Transfer and receive": "转账和接收" + "Usage Tutorial": "使用教程" } diff --git a/packages/status/public/perlin.js b/packages/status/public/perlin.js deleted file mode 100755 index 4404d3f2..00000000 --- a/packages/status/public/perlin.js +++ /dev/null @@ -1,408 +0,0 @@ -// Three JS - -const particleVert = ` -// -// GLSL textureless classic 3D noise "cnoise", -// with an RSL-style periodic variant "pnoise". -// Author: Stefan Gustavson (stefan.gustavson@liu.se) -// Version: 2011-10-11 -// -// Many thanks to Ian McEwan of Ashima Arts for the -// ideas for permutation and gradient selection. -// -// Copyright (c) 2011 Stefan Gustavson. All rights reserved. -// Distributed under the MIT license. See LICENSE file. -// https://github.com/ashima/webgl-noise -// - -vec3 mod289(vec3 x) -{ - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec4 mod289(vec4 x) -{ - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec4 permute(vec4 x) -{ - return mod289(((x*34.0)+1.0)*x); -} - -vec4 taylorInvSqrt(vec4 r) -{ - return 1.79284291400159 - 0.85373472095314 * r; -} - -vec3 fade(vec3 t) { - return t*t*t*(t*(t*6.0-15.0)+10.0); -} - -// Classic Perlin noise -float cnoise(vec3 P) -{ - vec3 Pi0 = floor(P); // Integer part for indexing - vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1 - Pi0 = mod289(Pi0); - Pi1 = mod289(Pi1); - vec3 Pf0 = fract(P); // Fractional part for interpolation - vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 - vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); - vec4 iy = vec4(Pi0.yy, Pi1.yy); - vec4 iz0 = Pi0.zzzz; - vec4 iz1 = Pi1.zzzz; - - vec4 ixy = permute(permute(ix) + iy); - vec4 ixy0 = permute(ixy + iz0); - vec4 ixy1 = permute(ixy + iz1); - - vec4 gx0 = ixy0 * (1.0 / 7.0); - vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; - gx0 = fract(gx0); - vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); - vec4 sz0 = step(gz0, vec4(0.0)); - gx0 -= sz0 * (step(0.0, gx0) - 0.5); - gy0 -= sz0 * (step(0.0, gy0) - 0.5); - - vec4 gx1 = ixy1 * (1.0 / 7.0); - vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; - gx1 = fract(gx1); - vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); - vec4 sz1 = step(gz1, vec4(0.0)); - gx1 -= sz1 * (step(0.0, gx1) - 0.5); - gy1 -= sz1 * (step(0.0, gy1) - 0.5); - - vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); - vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); - vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); - vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); - vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); - vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); - vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); - vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); - - vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); - g000 *= norm0.x; - g010 *= norm0.y; - g100 *= norm0.z; - g110 *= norm0.w; - vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); - g001 *= norm1.x; - g011 *= norm1.y; - g101 *= norm1.z; - g111 *= norm1.w; - - float n000 = dot(g000, Pf0); - float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); - float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); - float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); - float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); - float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); - float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); - float n111 = dot(g111, Pf1); - - vec3 fade_xyz = fade(Pf0); - vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); - vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); - float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); - return 2.2 * n_xyz; -} - -// Classic Perlin noise, periodic variant -float pnoise(vec3 P, vec3 rep) -{ - vec3 Pi0 = mod(floor(P), rep); // Integer part, modulo period - vec3 Pi1 = mod(Pi0 + vec3(1.0), rep); // Integer part + 1, mod period - Pi0 = mod289(Pi0); - Pi1 = mod289(Pi1); - vec3 Pf0 = fract(P); // Fractional part for interpolation - vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 - vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); - vec4 iy = vec4(Pi0.yy, Pi1.yy); - vec4 iz0 = Pi0.zzzz; - vec4 iz1 = Pi1.zzzz; - - vec4 ixy = permute(permute(ix) + iy); - vec4 ixy0 = permute(ixy + iz0); - vec4 ixy1 = permute(ixy + iz1); - - vec4 gx0 = ixy0 * (1.0 / 7.0); - vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; - gx0 = fract(gx0); - vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); - vec4 sz0 = step(gz0, vec4(0.0)); - gx0 -= sz0 * (step(0.0, gx0) - 0.5); - gy0 -= sz0 * (step(0.0, gy0) - 0.5); - - vec4 gx1 = ixy1 * (1.0 / 7.0); - vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; - gx1 = fract(gx1); - vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); - vec4 sz1 = step(gz1, vec4(0.0)); - gx1 -= sz1 * (step(0.0, gx1) - 0.5); - gy1 -= sz1 * (step(0.0, gy1) - 0.5); - - vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); - vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); - vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); - vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); - vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); - vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); - vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); - vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); - - vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); - g000 *= norm0.x; - g010 *= norm0.y; - g100 *= norm0.z; - g110 *= norm0.w; - vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); - g001 *= norm1.x; - g011 *= norm1.y; - g101 *= norm1.z; - g111 *= norm1.w; - - float n000 = dot(g000, Pf0); - float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); - float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); - float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); - float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); - float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); - float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); - float n111 = dot(g111, Pf1); - - vec3 fade_xyz = fade(Pf0); - vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); - vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); - float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); - return 1.5 * n_xyz; -} - -// Turbulence By Jaume Sanchez => https://codepen.io/spite/ - -varying vec2 vUv; -varying float noise; -varying float qnoise; -varying float displacement; - -uniform float time; -uniform float pointscale; -uniform float decay; -uniform float complex; -uniform float waves; -uniform float eqcolor; -uniform bool fragment; - -float turbulence( vec3 p) { - float t = - 0.1; - for (float f = 1.0 ; f <= 3.0 ; f++ ){ - float power = pow( 2.0, f ); - t += abs( pnoise( vec3( power * p ), vec3( 10.0, 10.0, 10.0 ) ) / power ); - } - return t; -} - -void main() { - - vUv = uv; - - noise = (1.0 * - waves) * turbulence( decay * abs(normal + time)); - qnoise = (2.0 * - eqcolor) * turbulence( decay * abs(normal + time)); - float b = pnoise( complex * (position) + vec3( 1.0 * time ), vec3( 100.0 ) ); - - if (fragment == true) { - displacement = - sin(noise) + normalize(b * 0.5); - } else { - displacement = - sin(noise) + cos(b * 0.5); - } - - vec3 newPosition = (position) + (normal * displacement); - gl_Position = (projectionMatrix * modelViewMatrix) * vec4( newPosition, 1.0 ); - gl_PointSize = (pointscale); - //gl_ClipDistance[0]; - -} -` - -const particleFrag = ` -varying float qnoise; - -uniform float time; -uniform bool redhell; - -void main() { - float r, g, b; - - - if (!redhell == true) { - r = cos(qnoise + 0.5); - g = cos(qnoise - 0.5); - b = 0.0; - } else { - r = cos(qnoise + 0.5); - g = cos(qnoise - 0.5); - b = abs(qnoise); - } - gl_FragColor = vec4(r, g, b, 1.0); -} -` - -window.addEventListener('startperlin', init, false) -window.addEventListener('playperlin', playperlin, false) -window.addEventListener('stopperlin', stopperlin, false) - -function init() { - createWorld() - createPrimitive() - //--- - animation() -} - -var Theme = { _darkred: 0x000000 } -var play = true -function playperlin() { - play = true -} - -function stopperlin() { - play = false -} - - -//-------------------------------------------------------------------- -var scene, camera, renderer, container -var start = Date.now() -var _width, _height -function createWorld() { - _width = window.innerWidth - _height = window.innerHeight - //--- - scene = new THREE.Scene() - //scene.fog = new THREE.Fog(Theme._darkred, 8, 20); - scene.background = new THREE.Color(Theme._darkred) - //--- - camera = new THREE.PerspectiveCamera(55, _width / _height, 1, 1000) - camera.position.z = 5.0 - //--- - renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }) - renderer.setSize(_width, _height) - //--- - container = document.getElementById('perlin-container') - if (container) { - container.appendChild(renderer.domElement) - } - //--- - window.addEventListener('resize', onWindowResize, false) -} - -function onWindowResize() { - _width = window.innerWidth - _height = window.innerHeight - renderer.setSize(_width, _height) - camera.aspect = _width / _height - camera.updateProjectionMatrix() -} - -//-------------------------------------------------------------------- - -var mat -var primitiveElement = function () { - this.mesh = new THREE.Object3D() - mat = new THREE.ShaderMaterial({ - wireframe: false, - //fog: true, - uniforms: { - time: { - type: 'f', - value: 0.0, - }, - pointscale: { - type: 'f', - value: 0.0, - }, - decay: { - type: 'f', - value: 0.0, - }, - complex: { - type: 'f', - value: 0.0, - }, - waves: { - type: 'f', - value: 0.0, - }, - eqcolor: { - type: 'f', - value: 0.0, - }, - fragment: { - type: 'i', - value: true, - }, - redhell: { - type: 'i', - value: true, - }, - }, - vertexShader: particleVert, - fragmentShader: particleFrag, - }) - var geo = new THREE.IcosahedronBufferGeometry(3, 7) - var mesh = new THREE.Points(geo, mat) - - //--- - this.mesh.add(mesh) -} - -var _primitive -function createPrimitive() { - _primitive = new primitiveElement() - scene.add(_primitive.mesh) -} - -//-------------------------------------------------------------------- - -var options = { - perlin: { - vel: 0.0015, - speed: 0.0001, - perlins: 1.0, - decay: 0.14, - complex: 0.3, - waves: 20.0, - eqcolor: 11.0, - fragment: false, - redhell: true, - }, - spin: { - sinVel: 0.0, - ampVel: 80.0, - }, -} - -function animation() { - lastFrame = requestAnimationFrame(animation) - - if (!play) { - return - } - - var performance = Date.now() * 0.003 - - _primitive.mesh.rotation.y += options.perlin.vel - _primitive.mesh.rotation.x = (Math.sin(performance * options.spin.sinVel) * options.spin.ampVel * Math.PI) / 180 - //--- - mat.uniforms['time'].value = options.perlin.speed * (Date.now() - start) - mat.uniforms['pointscale'].value = options.perlin.perlins - mat.uniforms['decay'].value = options.perlin.decay - mat.uniforms['complex'].value = options.perlin.complex - mat.uniforms['waves'].value = options.perlin.waves - mat.uniforms['eqcolor'].value = options.perlin.eqcolor - mat.uniforms['fragment'].value = options.perlin.fragment - mat.uniforms['redhell'].value = options.perlin.redhell - //--- - camera.lookAt(scene.position) - renderer.render(scene, camera) -} diff --git a/packages/status/src/components/Contacts/discord.svg b/packages/status/src/components/Contacts/discord.svg new file mode 100644 index 00000000..bddaad6a --- /dev/null +++ b/packages/status/src/components/Contacts/discord.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/status/src/components/Contacts/github.svg b/packages/status/src/components/Contacts/github.svg new file mode 100644 index 00000000..1ec927ed --- /dev/null +++ b/packages/status/src/components/Contacts/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/status/src/components/Contacts/index.module.scss b/packages/status/src/components/Contacts/index.module.scss new file mode 100644 index 00000000..7e42bcc8 --- /dev/null +++ b/packages/status/src/components/Contacts/index.module.scss @@ -0,0 +1,5 @@ +.contacts { + display: flex; + gap: 32px; + align-items: center; +} diff --git a/packages/status/src/components/Contacts/index.tsx b/packages/status/src/components/Contacts/index.tsx new file mode 100644 index 00000000..1b3d5a86 --- /dev/null +++ b/packages/status/src/components/Contacts/index.tsx @@ -0,0 +1,27 @@ +import { ComponentProps, FC } from 'react' +import Link from 'next/link' +import clsx from 'clsx' +import IconX from './x.svg' +import IconDiscord from './discord.svg' +import IconGithub from './github.svg' +import styles from './index.module.scss' + +export const Contacts: FC & { linkClass?: string; iconClass?: string }> = ({ + linkClass, + iconClass, + ...divProps +}) => { + return ( +
+ + + + + + + + + +
+ ) +} diff --git a/packages/status/src/components/Contacts/x.svg b/packages/status/src/components/Contacts/x.svg new file mode 100644 index 00000000..e4779c01 --- /dev/null +++ b/packages/status/src/components/Contacts/x.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/status/src/components/Footer/full-logo.svg b/packages/status/src/components/Footer/full-logo.svg new file mode 100644 index 00000000..b52abf23 --- /dev/null +++ b/packages/status/src/components/Footer/full-logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/status/src/components/Footer/index.module.scss b/packages/status/src/components/Footer/index.module.scss new file mode 100644 index 00000000..0a62d477 --- /dev/null +++ b/packages/status/src/components/Footer/index.module.scss @@ -0,0 +1,150 @@ +@import '../../styles/variables.module'; + +.footer { + padding: 120px var(--contentWrapperPadding); + border-top: 1px solid #2e2e2e; + + @media (max-width: $mobileBreakPoint) { + padding: 48px var(--contentWrapperPadding) 104px var(--contentWrapperPadding); + } + + .content { + display: flex; + justify-content: space-between; + max-width: var(--contentAreaWidth); + margin: 0 auto; + + .left { + display: flex; + flex-direction: column; + } + + .right { + display: flex; + gap: 104px; + } + + @media (max-width: $mobileBreakPoint) { + flex-direction: column; + gap: 40px; + + .right { + flex-direction: column; + gap: 40px; + } + } + } +} + +.serversState { + display: flex; + gap: 4px; + align-items: center; + margin-top: 24px; + color: #00cc9b; + font-size: 14px; + line-height: 16px; + + .title { + color: #f5f5f5; + font-weight: 600; + } + + .dot { + width: 6px; + height: 6px; + margin-left: 4px; + background: #00cc9b; + border-radius: 50%; + } + + &.warnning { + // TODO: need design + color: chocolate; + + .dot { + background: chocolate; + } + } + + @media (max-width: $mobileBreakPoint) { + margin-top: 40px; + + .title { + font-size: 16px; + } + } +} + +.serviceMonitor { + margin-top: 24px; + color: #f5f5f5; + font-weight: 600; + font-size: 14px; + line-height: 16px; + + @media (max-width: $mobileBreakPoint) { + font-size: 16px; + } +} + +.contacts { + gap: 40px; + margin-top: auto; + + @media (max-width: $mobileBreakPoint) { + margin-top: 40px; + } +} + +.copyright { + margin-top: 36px; + color: #666; + font-size: 12px; + line-height: 16px; + + @media (max-width: $mobileBreakPoint) { + margin-top: 0; + } +} + +.linkList { + display: flex; + flex-direction: column; + gap: 32px; + + .title { + font-weight: 600; + font-size: 14px; + line-height: 16px; + + @media (max-width: $mobileBreakPoint) { + font-size: 16px; + } + } + + .linksContainer { + display: flex; + flex-direction: column; + gap: 24px 0; + + a { + color: #999; + font-size: 14px; + line-height: 16px; + + &:hover { + color: #f5f5f5; + } + } + + @media (max-width: $mobileBreakPoint) { + flex-flow: row wrap; + column-gap: 20%; + + a { + flex: 1 1 40%; + } + } + } +} diff --git a/packages/status/src/components/Footer/index.tsx b/packages/status/src/components/Footer/index.tsx new file mode 100644 index 00000000..c1eb9058 --- /dev/null +++ b/packages/status/src/components/Footer/index.tsx @@ -0,0 +1,95 @@ +import { ComponentProps, FC } from 'react' +import clsx from 'clsx' +import { useTranslation } from 'react-i18next' +import Link from 'next/link' +import { useIsMobile } from '@magickbase-website/shared' +import styles from './index.module.scss' +import IconFullLogo from './full-logo.svg' +import { Contacts } from '../Contacts' +import { LinkWithEffect } from '../UpsideDownEffect' + +export type FooterProps = ComponentProps<'div'> & { + serviceState?: 'operational' | 'downtime' | 'degraded' | 'unknown' + serviceLink?: string +} + +export const Footer: FC = ({ + serviceState, + serviceLink = 'https://status.magickbase.com/', + ...elProps +}) => { + const { t } = useTranslation('common') + const isMobile = useIsMobile() + + const serviceStateText = + serviceState === 'operational' + ? t('All services are online') + : serviceState === 'downtime' || serviceState === 'degraded' + ? t('Some services are offline') + : t('Services status loading...') + + return ( +
+
+
+ + +
+ {t('Status')} +
+ + {serviceStateText} + +
+ + +
{t('Service Monitor')}
+ + + + + {!isMobile &&
{t('Copyright © 2023 Magickbase All Rights Reserved.')}
} +
+ +
+
+
{t('Services')}
+
+ + {t('Neuron Wallet')} + + + {t('CKB Explorer')} + + + {t('Godwoke Explorer')} + + + {t('Axon Explorer')} + + + {t('Kuai')} + + + {t('Lumos')} + +
+
+ +
+
{t('Foundation')}
+
+ {t('Nervos')} +
+
+ + {isMobile &&
{t('Copyright © 2023 Magickbase All Rights Reserved.')}
} +
+
+
+ ) +} diff --git a/packages/status/src/components/Header/close.svg b/packages/status/src/components/Header/close.svg new file mode 100644 index 00000000..a8d79832 --- /dev/null +++ b/packages/status/src/components/Header/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/status/src/components/Header/github.svg b/packages/status/src/components/Header/github.svg new file mode 100644 index 00000000..95cbdb51 --- /dev/null +++ b/packages/status/src/components/Header/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/status/src/components/Header/index.module.scss b/packages/status/src/components/Header/index.module.scss new file mode 100644 index 00000000..8e1312f0 --- /dev/null +++ b/packages/status/src/components/Header/index.module.scss @@ -0,0 +1,229 @@ +/* stylelint-disable custom-property-pattern */ +/* stylelint-disable no-descending-specificity */ +@import '../../styles/variables.module'; + +.header, +.headerMobile { + position: sticky; + top: 0; + z-index: 1; + padding: 0 var(--contentWrapperPadding); + + // TODO: The design has not been fully implemented here, and there are some bugs in Chrome/Firefox. + background: rgb(255 255 255 / 1%) linear-gradient(180deg, rgb(0 0 0 / 40%) 0%, rgb(0 0 0 / 20%) 100%); + backdrop-filter: blur(30px); +} + +$desktopHeaderContentHeight: 88px; + +.header { + .content { + display: flex; + justify-content: space-between; + max-width: var(--contentAreaWidth); + height: $desktopHeaderContentHeight; + margin: 0 auto; + + .left { + display: flex; + gap: 64px; + align-items: center; + height: 100%; + font-weight: 600; + font-size: 16px; + + > a { + display: flex; + align-items: center; + height: 100%; + + &:hover { + color: #00cc9b; + } + } + } + + .right { + display: flex; + gap: 48px; + align-items: center; + } + } +} + +.headerMobile { + .top { + display: flex; + justify-content: space-between; + height: 64px; + + .left { + display: flex; + gap: 64px; + align-items: center; + height: 100%; + + > a { + display: flex; + + &:hover { + color: #00cc9b; + } + } + } + + .right { + display: flex; + gap: 48px; + align-items: center; + } + } +} + +.menuDialog { + &Trigger { + display: flex; + cursor: pointer; + + @media (max-width: $mobileBreakPoint) { + & > svg { + width: 56px; + height: auto; + } + } + } + + &Overlay { + position: fixed; + z-index: 1; + background: rgba(#000, 0.8); + inset: 0; + + @media (max-width: $mobileBreakPoint) { + background: #000; + } + } + + &Content { + // This approach may not be conducive to understanding and maintenance, but development is quite convenient for now. + // If maintenance becomes difficult later, it can be changed to control based on JS + CSS variables. + --menuIconRightOffset: calc(max(calc((100vw - var(--contentAreaWidth)) / 2), var(--contentWrapperPadding))); + + position: fixed; + top: 0; + right: 0; + bottom: 0; + z-index: 10; + display: flex; + flex-direction: column; + padding: 0 var(--menuIconRightOffset) 96px 96px; + color: #f5f5f5; + background: #111; + + &:focus-visible { + outline: none; + } + + .top { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: right; + height: $desktopHeaderContentHeight; + + .left { + > a { + display: flex; + } + } + } + + .content { + margin: 64px 0; + } + + .close { + cursor: pointer; + } + + .title { + display: flex; + margin-top: 64px; + font-weight: 600; + font-size: 32px; + + &:first-child { + margin-top: 0; + } + } + + .links { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 40px 72px; + margin-top: 48px; + color: #999; + font-size: 26px; + + & > a { + &:hover { + color: #f5f5f5; + } + + &.selected { + color: #f5f5f5; + } + } + } + + .contacts { + gap: 48px; + margin-top: auto; + } + + @media (max-width: $mobileBreakPoint) { + left: 0; + padding: 0 var(--contentWrapperPadding); + border-radius: 40px 40px 0 0; + + .top { + justify-content: space-between; + height: 64px; + } + + .close { + width: 56px; + height: auto; + } + + .content { + margin: 0; + } + + .title { + margin-top: 40px; + font-size: 16px; + + &:first-child { + margin-top: 32px; + } + } + + .links { + gap: 24px 64px; + margin-top: 32px; + font-size: 14px; + } + + .languages { + display: flex; + flex-direction: column; + gap: 32px; + } + + .contacts { + margin-top: 64px; + } + } + } +} diff --git a/packages/status/src/components/Header/index.tsx b/packages/status/src/components/Header/index.tsx new file mode 100644 index 00000000..bfbe7699 --- /dev/null +++ b/packages/status/src/components/Header/index.tsx @@ -0,0 +1,163 @@ +import clsx from 'clsx' +import { ComponentProps, FC } from 'react' +import Link from 'next/link' +import * as Dialog from '@radix-ui/react-dialog' +import { useRouter } from 'next/router' +import { useTranslation } from 'react-i18next' +import { OverlayScrollbarsComponent } from 'overlayscrollbars-react' +import { useIsMobile, languages } from '@magickbase-website/shared' +import styles from './index.module.scss' +import IconLogo from './logo.svg' +import IconGithub from './github.svg' +import IconMenu from './menu.svg' +import IconClose from './close.svg' +import { Contacts } from '../Contacts' +import { LinkWithEffect } from '../UpsideDownEffect' + +export type HeaderProps = ComponentProps<'div'> & { + navMenuGroupName?: string | null + navMenus?: { name: string; link: string }[] + githubLink?: string +} + +export const Header: FC = props => { + const isMobile = useIsMobile() + return isMobile ? : +} + +export const Header$Desktop: FC = ({ navMenuGroupName, navMenus, githubLink, ...elProps }) => { + return ( +
+
+
+ + + + {navMenus?.map(({ name, link }) => ( + + {name} + + ))} +
+ +
+ {githubLink && ( + + + + )} + + +
+
+
+ ) +} + +export const Header$Mobile: FC = ({ navMenuGroupName, navMenus, ...elProps }) => { + return ( +
+
+
+ + + +
+ +
+ +
+
+
+ ) +} + +const MenuDialog: FC> = ({ navMenuGroupName, navMenus }) => { + const { t } = useTranslation('common') + const isMobile = useIsMobile() + const router = useRouter() + const { pathname, query } = router + + return ( + + + {/* This div wrapping layer is because SVGComponent cannot accept the ref from Dialog.Trigger, it will error. */} +
+ +
+
+ + + + +
+ {isMobile && ( +
+ + + +
+ )} + + + + +
+ + +
+ {navMenuGroupName != null && navMenus != null && ( + <> +
{navMenuGroupName}
+
+ {navMenus.map(({ name, link }) => ( + + {name} + + ))} +
+ + )} + + + {t('Home')} + + +
{t('Services')}
+
+ {t('Neuron Wallet')} + {t('CKB Explorer')} + {t('Godwoke Explorer')} + {t('Axon Explorer')} + {t('Lumos')} + {t('Kuai')} +
+ + + {t('Service Monitor')} + + +
{t('Language')}
+
+ {languages.map(language => ( + + {language.name} + + ))} +
+
+
+ + +
+
+
+ ) +} diff --git a/packages/status/src/components/Header/logo.svg b/packages/status/src/components/Header/logo.svg new file mode 100644 index 00000000..05a7a07b --- /dev/null +++ b/packages/status/src/components/Header/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/status/src/components/Header/menu.svg b/packages/status/src/components/Header/menu.svg new file mode 100644 index 00000000..ca7cfb54 --- /dev/null +++ b/packages/status/src/components/Header/menu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/status/src/components/Layout/index.tsx b/packages/status/src/components/Layout/index.tsx index cee491cf..f8ad1917 100644 --- a/packages/status/src/components/Layout/index.tsx +++ b/packages/status/src/components/Layout/index.tsx @@ -1,18 +1,21 @@ -import { Footer, Header } from '@magickbase-website/shared' +import { useTranslation } from 'next-i18next' +import { Footer } from '@/components/Footer' +import { Header } from '@/components/Header' import { api } from '@/utils/api' import { PropsWithChildren } from 'react' export const Layout: React.FC = ({ children }) => { + const { t } = useTranslation('common') const aggregateStateQuery = api.uptime.aggregateState.useQuery() return ( <>
diff --git a/packages/status/src/components/StatusResource/index.tsx b/packages/status/src/components/StatusResource/index.tsx index 5bc6aa0a..7bdd0577 100644 --- a/packages/status/src/components/StatusResource/index.tsx +++ b/packages/status/src/components/StatusResource/index.tsx @@ -1,5 +1,6 @@ import classnames from 'classnames' import { ComponentProps } from 'react' +import { useTranslation } from 'react-i18next' import { StatusResourceResponse } from '@/types' import toast from 'react-hot-toast' import { Tooltip } from '@/components/Tooltip' @@ -26,6 +27,7 @@ function parseDuration(duration: number) { } export const StatusResource: React.FC = ({ link, resource, ...props }) => { + const { t } = useTranslation('common') const isMobile = useIsMobile(); const [_, copyToClipboard] = useCopyToClipboard() const currentStatus = resource.attributes.status_history[resource.attributes.status_history.length - 1] @@ -45,7 +47,7 @@ export const StatusResource: React.FC = ({ link, resource, className="cursor-pointer fill-white hover:fill-[#00CC9B] transition-all" onClick={async () => { await copyToClipboard(link) - toast.success('Copied') + toast.success(t('copied')) }} />
@@ -69,7 +71,7 @@ export const StatusResource: React.FC = ({ link, resource, - {(resource.attributes.availability * 100).toPrecision(5)}% Normal + {(resource.attributes.availability * 100).toPrecision(5)}% {t('normal')}
@@ -114,8 +116,8 @@ export const StatusResource: React.FC = ({ link, resource,
- {Math.min(resource.attributes.status_history.length, length)} days ago - Today + {Math.min(resource.attributes.status_history.length, length)} {t('days_ago')} + {t('today')}
) diff --git a/packages/status/src/components/UpsideDownEffect/index.module.scss b/packages/status/src/components/UpsideDownEffect/index.module.scss new file mode 100644 index 00000000..a9c3ae3e --- /dev/null +++ b/packages/status/src/components/UpsideDownEffect/index.module.scss @@ -0,0 +1,76 @@ +@import '../../styles/variables.module'; + +.upsideDownEffect { + position: relative; + display: inline-flex; + overflow: hidden; + + .nodes { + display: flex; + flex-direction: column; + align-items: center; + transition: transform 0.4s ease; + + .content, + .cloned { + display: block; + overflow: hidden; + text-overflow: ellipsis; + transition: transform 0.4s ease; + } + + .content { + transform-origin: right center; + } + + .cloned { + position: absolute; + top: 100%; + transform: rotate(20deg); + transform-origin: left center; + } + } + + &.fullWidth { + width: 100%; + + .nodes { + width: 100%; + + .content, + .cloned { + width: 100%; + } + } + } + + &.fullHeight { + height: 100%; + + .nodes { + height: 100%; + + .content, + .cloned { + height: 100%; + } + } + } +} + +.hoverableContainer, +.upsideDownEffect { + &:hover { + .nodes { + transform: translateY(-100%); + + .content { + transform: rotate(20deg); + } + + .cloned { + transform: rotate(0); + } + } + } +} diff --git a/packages/status/src/components/UpsideDownEffect/index.tsx b/packages/status/src/components/UpsideDownEffect/index.tsx new file mode 100644 index 00000000..1c63051d --- /dev/null +++ b/packages/status/src/components/UpsideDownEffect/index.tsx @@ -0,0 +1,88 @@ +import { ComponentProps, FC, ReactNode } from 'react' +import Link from 'next/link' +import clsx from 'clsx' +import { useIsMobile } from '@magickbase-website/shared' +import styles from './index.module.scss' + +interface UpsideDownEffectProps { + content?: ReactNode | null + children?: ReactNode | ((hoverableContainerClass: string, contentWithEffect: ReactNode) => ReactNode) + fullWidth?: boolean + fullHeight?: boolean + nodeClass?: string +} + +export const UpsideDownEffect: FC, 'children' | 'content'> & UpsideDownEffectProps> = ({ + children, + content, + fullWidth, + fullHeight, + nodeClass, + ...outerProps +}) => { + const computedContent = content ?? (typeof children === 'function' ? '' : children) + + if (typeof children === 'function') { + return children( + styles.hoverableContainer ?? '', + + + {computedContent} + + {computedContent} + + + , + ) + } + + return ( + + + {computedContent} + + {computedContent} + + + + ) +} + +export const LinkWithEffect: FC & UpsideDownEffectProps> = ({ + children, + content, + fullWidth, + fullHeight, + nodeClass, + ...linkProps +}) => { + const isMobile = useIsMobile() + + if (isMobile) return {children} + + if (typeof children === 'function') throw new Error('LinkWithEffect not support children as function') + + return ( + + {(hoverableContainerClass, contentWithEffect) => ( + + {contentWithEffect} + + )} + + ) +} diff --git a/packages/status/src/pages/events.tsx b/packages/status/src/pages/events.tsx index 752bb8a2..ac9b4d3e 100644 --- a/packages/status/src/pages/events.tsx +++ b/packages/status/src/pages/events.tsx @@ -1,13 +1,22 @@ import dayjs from 'dayjs' import { FC, useState } from 'react' +import { Trans, useTranslation } from 'next-i18next' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import classnames from 'classnames' import { api } from '@/utils/api' import { Layout } from '@/components/Layout' import NextIcon from './next.svg' import PreviousIcon from './previous.svg' +export const getServerSideProps = async ({ locale }: { locale: string }) => ({ + props: { + ...(await serverSideTranslations(locale ?? 'en', ['common'])), + }, +}) + const EventPage: FC<{ page?: number }> = ({ page = 1 }) => { const incidentsQuery = api.uptime.listStatusIncidents.useQuery({ page }) + const { t } = useTranslation('common') if (!incidentsQuery.data) { return new Array(10) @@ -30,12 +39,19 @@ const EventPage: FC<{ page?: number }> = ({ page = 1 }) => { {dayjs(incident.attributes.resolved_at).format(intraday ? 'hh:mm' : 'YYYY-MM-DD hh:mm')}
- {incident.attributes.name} is down due to {incident.attributes.cause} +
{incident.attributes.name}
-
Downtime
+
{t('downtime')}
) @@ -45,6 +61,7 @@ const EventPage: FC<{ page?: number }> = ({ page = 1 }) => { export default function Event() { const [page, setPage] = useState(1) const [goto, setGoto] = useState('') + const { t } = useTranslation('common') const incidentPages = api.uptime.countIncidentPages.useQuery() @@ -53,7 +70,7 @@ export default function Event() { return (
-
History Events
+
{t('history_events')}
@@ -68,11 +85,7 @@ export default function Event() { First - @@ -105,7 +118,9 @@ export default function Event() { setGoto(Number.isNaN(parseInt(e.target.value)) ? '' : parseInt(e.target.value).toString())} + onChange={e => + setGoto(Number.isNaN(parseInt(e.target.value)) ? '' : parseInt(e.target.value).toString()) + } />