Skip to content

Commit 9eaf88b

Browse files
committed
feat: Add Galaxy login page with background animation and email submission form
1 parent ddaecf1 commit 9eaf88b

File tree

6 files changed

+305
-1
lines changed

6 files changed

+305
-1
lines changed

client/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Routes, Route } from 'react-router-dom'
55
import Table from './components/View/TableView/Table'
66
import { SocketProvider } from './context/Socket/SocketProvider'
77
import Home from './pages/Home/Home'
8+
import Galaxy from './pages/Login/Galaxy'
89

910
function App() {
1011
return (
@@ -16,6 +17,7 @@ function App() {
1617
<Route path='/' element={<Table />} />
1718
<Route path='/login/:email/:magicLink' element={<Home loggingIn={true} />} />
1819
<Route path='/dashboard' element={<Dashboard />} />
20+
<Route path='/galaxy' element={<Galaxy />} />
1921
</Routes>
2022
</SocketProvider>
2123
</WorkflowProvider>

client/src/index.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,31 @@ table-hover td,
2323
--tw-bg-opacity: 1;
2424
background-color: rgb(243 244 246 / var(--tw-bg-opacity)) /* #f3f4f6 */;
2525
}
26+
27+
.container-gradient {
28+
background-image: radial-gradient(1600px at 70% 120%, rgba(33, 39, 80, 1) 10%, #020409 100%);
29+
}
30+
31+
.container-gradient-dark {
32+
background-image: radial-gradient(1600px at 70% 120%, rgb(92, 107, 206) 10%, #02040950 100%);
33+
}
34+
35+
.w-inherit {
36+
width: inherit;
37+
}
38+
.h-inherit {
39+
height: inherit;
40+
}
41+
42+
@keyframes fadeIn {
43+
from {
44+
opacity: 0;
45+
}
46+
to {
47+
opacity: 1;
48+
}
49+
}
50+
51+
.fade-in {
52+
animation: fadeIn 0.5s ease-in-out;
53+
}

client/src/pages/Home/Home.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import icon from '../../icons/icon.svg'
44

55
import { useEffect, useState } from 'react'
66
import { useNavigate, useParams } from 'react-router-dom'
7+
import { API_URL } from '../../service/api'
78

89
type HomeProps = {
910
loggingIn?: boolean
@@ -29,7 +30,7 @@ const Home = ({ loggingIn }: HomeProps) => {
2930
setLoading(true)
3031

3132
try {
32-
const response = await fetch('http://localhost:8000/api/login', {
33+
const response = await fetch(`${API_URL}/api/login`, {
3334
method: 'POST',
3435
headers: {
3536
'Content-Type': 'application/json',
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { render } from '@testing-library/react'
3+
import { BrowserRouter as Router } from 'react-router-dom'
4+
import Galaxy from './Galaxy'
5+
6+
describe('Galaxy', () => {
7+
it('should render Login component', () => {
8+
const { getByTestId } = render(
9+
<Router>
10+
<Galaxy />
11+
</Router>,
12+
)
13+
const input = getByTestId('email-input')
14+
expect(input).toBeTruthy()
15+
})
16+
})

client/src/pages/Login/Galaxy.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useEffect, useRef, useState } from 'react'
2+
import { createUniverse } from './stars-render'
3+
import { API_URL } from '../../service/api'
4+
5+
const Galaxy = () => {
6+
const canvasRef = useRef<HTMLCanvasElement | null>(null)
7+
const emailRef = useRef<HTMLInputElement | null>(null)
8+
9+
const [submitting, setSubmitting] = useState(false)
10+
11+
useEffect(() => {
12+
const starDensity = 0.216
13+
const speedCoeff = 0.05
14+
const giantColor = '180,184,240'
15+
const starColor = '226,225,142'
16+
const cometColor = '226,225,224'
17+
const first = true
18+
19+
const canva = canvasRef.current!
20+
const cleanup = createUniverse(
21+
canva,
22+
starDensity,
23+
speedCoeff,
24+
giantColor,
25+
starColor,
26+
cometColor,
27+
first,
28+
)
29+
30+
return () => {
31+
cleanup()
32+
}
33+
}, [])
34+
35+
const submitEmail = async (e: React.FormEvent) => {
36+
setSubmitting(true)
37+
e.preventDefault()
38+
const email = emailRef.current!.value
39+
await fetch(`${API_URL}/login`, {
40+
method: 'POST',
41+
headers: {
42+
'Content-Type': 'application/json',
43+
},
44+
body: JSON.stringify({ email }),
45+
})
46+
.then((res) => {
47+
if (!res.ok) {
48+
throw new Error('Something went wrong!')
49+
}
50+
})
51+
.then(await new Promise((resolve) => setTimeout(resolve, 3000)))
52+
.finally(() => {
53+
setSubmitting(false)
54+
})
55+
}
56+
57+
return (
58+
<div className='p-0 m-0 w-full h-full fixed flex justify-center items-center contrast-120'>
59+
<div className='w-full h-full container-gradient'>
60+
<div className='w-inherit h-inherit'>
61+
<canvas ref={canvasRef} id='universe' className='w-full h-full'></canvas>
62+
<div className='absolute top-1/3 w-full text-center'>
63+
<h1 className='text-5xl text-gray-50 pb-4'>Atlas</h1>
64+
<p className='text-base text-gray-50'>
65+
A simple and fun way to explore the universe of science. <br />
66+
Sign up now to get started.
67+
</p>
68+
69+
<form onSubmit={submitEmail} className='flex flex-col justify-center items-center'>
70+
{submitting ? (
71+
<p className='text-slate-200 pt-4 fade-in'>Check your email for the magic link!</p>
72+
) : (
73+
<input
74+
data-testid='email-input'
75+
type='email'
76+
placeholder='Email'
77+
className='input input-sm w-72 mt-4'
78+
ref={emailRef}
79+
/>
80+
)}
81+
<button className='mt-3 border rounded border-gray-300 px-2 opacity-40 text-[#edf3fe] transition-opacity duration-400 ease hover:opacity-100 hover:border-white'>
82+
Sign up / Login
83+
</button>
84+
</form>
85+
</div>
86+
</div>
87+
</div>
88+
</div>
89+
)
90+
}
91+
92+
export default Galaxy
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
export class Star {
2+
x!: number
3+
y!: number
4+
r!: number
5+
dx!: number
6+
dy!: number
7+
fadingOut: boolean | null = null
8+
fadingIn = true
9+
opacity = 0
10+
opacityTresh!: number
11+
do!: number
12+
giant!: boolean
13+
comet!: boolean
14+
15+
constructor(
16+
private width: number,
17+
private height: number,
18+
private speedCoeff: number,
19+
private universe: CanvasRenderingContext2D | null,
20+
private colors: { giantColor: string; starColor: string; cometColor: string },
21+
private first: boolean,
22+
) {}
23+
24+
reset() {
25+
const { getRandInterval, getProbability } = this.constructor as typeof Star
26+
this.giant = getProbability(3)
27+
this.comet = this.giant || this.first ? false : getProbability(10)
28+
this.x = getRandInterval(0, this.width - 10)
29+
this.y = getRandInterval(0, this.height)
30+
this.r = getRandInterval(1.1, 2.6)
31+
this.dx =
32+
getRandInterval(this.speedCoeff, 6 * this.speedCoeff) +
33+
(this.comet ? this.speedCoeff * getRandInterval(50, 120) : 0) +
34+
this.speedCoeff * 2
35+
this.dy =
36+
-getRandInterval(this.speedCoeff, 6 * this.speedCoeff) -
37+
(this.comet ? this.speedCoeff * getRandInterval(50, 120) : 0)
38+
this.opacityTresh = getRandInterval(0.2, 1 - (this.comet ? 0.4 : 0))
39+
this.do = getRandInterval(0.0005, 0.002) + (this.comet ? 0.001 : 0)
40+
this.fadingOut = null
41+
this.fadingIn = true
42+
}
43+
44+
fadeIn() {
45+
if (this.fadingIn) {
46+
this.fadingIn = this.opacity > this.opacityTresh ? false : true
47+
this.opacity += this.do
48+
}
49+
}
50+
51+
fadeOut() {
52+
if (this.fadingOut) {
53+
this.fadingOut = this.opacity < 0 ? false : true
54+
this.opacity -= this.do / 2
55+
if (this.x > this.width || this.y < 0) {
56+
this.fadingOut = false
57+
this.reset()
58+
}
59+
}
60+
}
61+
62+
draw() {
63+
if (!this.universe) return
64+
65+
const { universe } = this
66+
universe.beginPath()
67+
68+
if (this.giant) {
69+
universe.fillStyle = `rgba(${this.colors.giantColor},${this.opacity})`
70+
universe.arc(this.x, this.y, 2, 0, 2 * Math.PI, false)
71+
} else if (this.comet) {
72+
universe.fillStyle = `rgba(${this.colors.cometColor},${this.opacity})`
73+
universe.arc(this.x, this.y, 1.5, 0, 2 * Math.PI, false)
74+
75+
for (let i = 0; i < 30; i++) {
76+
universe.fillStyle = `rgba(${this.colors.cometColor},${this.opacity - (this.opacity / 20) * i})`
77+
universe.rect(this.x - (this.dx / 4) * i, this.y - (this.dy / 4) * i - 2, 2, 2)
78+
universe.fill()
79+
}
80+
} else {
81+
universe.fillStyle = `rgba(${this.colors.starColor},${this.opacity})`
82+
universe.rect(this.x, this.y, this.r, this.r)
83+
}
84+
85+
universe.closePath()
86+
universe.fill()
87+
}
88+
89+
move() {
90+
this.x += this.dx
91+
this.y += this.dy
92+
if (this.fadingOut === false) {
93+
this.reset()
94+
}
95+
if (this.x > this.width - this.width / 4 || this.y < 0) {
96+
this.fadingOut = true
97+
}
98+
}
99+
100+
static getProbability(percents: number): boolean {
101+
return Math.floor(Math.random() * 1000) + 1 < percents * 10
102+
}
103+
104+
static getRandInterval(min: number, max: number): number {
105+
return Math.random() * (max - min) + min
106+
}
107+
}
108+
109+
export function createUniverse(
110+
canva: HTMLCanvasElement,
111+
starDensity: number,
112+
speedCoeff: number,
113+
giantColor: string,
114+
starColor: string,
115+
cometColor: string,
116+
first: boolean,
117+
) {
118+
const universe: CanvasRenderingContext2D | null = canva.getContext('2d')
119+
const stars: Star[] = []
120+
let width = window.innerWidth
121+
let height = window.innerHeight
122+
let starCount = width * starDensity
123+
124+
const windowResizeHandler = () => {
125+
width = window.innerWidth
126+
height = window.innerHeight
127+
starCount = width * starDensity
128+
canva.setAttribute('width', width.toString())
129+
canva.setAttribute('height', height.toString())
130+
}
131+
132+
function draw() {
133+
if (!universe) return
134+
universe.clearRect(0, 0, width, height)
135+
for (const star of stars) {
136+
star.move()
137+
star.fadeIn()
138+
star.fadeOut()
139+
star.draw()
140+
}
141+
requestAnimationFrame(draw)
142+
}
143+
144+
windowResizeHandler()
145+
window.addEventListener('resize', windowResizeHandler)
146+
147+
for (let i = 0; i < starCount; i++) {
148+
const star = new Star(
149+
width,
150+
height,
151+
speedCoeff,
152+
universe,
153+
{ giantColor, starColor, cometColor },
154+
first,
155+
)
156+
star.reset()
157+
stars.push(star)
158+
}
159+
160+
draw()
161+
162+
return () => {
163+
window.removeEventListener('resize', windowResizeHandler)
164+
}
165+
}

0 commit comments

Comments
 (0)