Adding the skeleton QR code
This commit is contained in:
12
frontend/src/App.tsx
Normal file
12
frontend/src/App.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import QRComponent from '@templates/QrCodeComponent';
|
||||
import styles from '@styles/App.module.scss';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<QRComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
77
frontend/src/components/organisms/QRForm.tsx
Normal file
77
frontend/src/components/organisms/QRForm.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import styles from '@styles/QRForm.module.scss';
|
||||
|
||||
const QRForm: React.FC = () => {
|
||||
const [text, setText] = useState('');
|
||||
const [size, setSize] = useState<'150' | '300' | '600'>('300');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setImageUrl(null);
|
||||
|
||||
if (!text.trim()) {
|
||||
setError('Please enter a URL or text to encode.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Call a mock API to simulate server-side generation step.
|
||||
// Using jsonplaceholder as a harmless mock endpoint.
|
||||
await axios.post('https://jsonplaceholder.typicode.com/posts', {
|
||||
payload: text,
|
||||
size,
|
||||
});
|
||||
|
||||
// Build a QR image URL using a public QR generator for preview purposes.
|
||||
// In a real implementation the server would return the final image URL or binary data.
|
||||
const generatedUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(
|
||||
text
|
||||
)}`;
|
||||
setImageUrl(generatedUrl);
|
||||
} catch (err) {
|
||||
setError('Failed to generate QR code. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.qrFormBody}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label className={styles.qrFormLabel}>Submit URL or text</label>
|
||||
<input
|
||||
type="text"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="https://example.com or some text"
|
||||
className={styles.qrFormInput}
|
||||
/>
|
||||
|
||||
<label className={styles.qrFormLabel}>Image size</label>
|
||||
<select
|
||||
value={size}
|
||||
onChange={(e) => setSize(e.target.value as '150' | '300' | '600')}
|
||||
className={styles.qrFormSelect}
|
||||
>
|
||||
<option value="150">Small — 150x150</option>
|
||||
<option value="300">Medium — 300x300</option>
|
||||
<option value="600">Large — 600x600</option>
|
||||
</select>
|
||||
|
||||
{error && <div className={styles.qrError}>{error}</div>}
|
||||
|
||||
<button type="submit" disabled={loading} className={styles.qrFormButton}>
|
||||
{loading ? 'Generating…' : 'Generate'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRForm;
|
||||
42
frontend/src/components/organisms/QRImage.tsx
Normal file
42
frontend/src/components/organisms/QRImage.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import styles from '@styles/QRImage.module.scss';
|
||||
|
||||
type Props = {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
filename?: string;
|
||||
};
|
||||
|
||||
const QRImage = ({ src, alt = 'QR code', filename = 'qrcode.png' }: Props) => {
|
||||
const imgRef = React.useRef<HTMLImageElement | null>(null);
|
||||
|
||||
const handleDownload = () => {
|
||||
const img = imgRef.current;
|
||||
if (!img || !img.src) {
|
||||
return;
|
||||
}
|
||||
const link = document.createElement('a');
|
||||
link.href = img.src;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.qrImageBody}>
|
||||
<div className={styles.qrImageContainer}>
|
||||
{src ? (
|
||||
<img ref={imgRef} src={src} alt={alt} className={styles.qrImg} />
|
||||
) : (
|
||||
<span className={styles.qrSpan}>No image</span>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" onClick={handleDownload} className={styles.qrButton}>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRImage;
|
||||
11
frontend/src/components/organisms/Title.tsx
Normal file
11
frontend/src/components/organisms/Title.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import styles from '@styles/title.module.scss';
|
||||
|
||||
const Title: React.FC = () => (
|
||||
<header className={styles.title}>
|
||||
<h1>QRcode</h1>
|
||||
<h2>generator</h2>
|
||||
</header>
|
||||
);
|
||||
|
||||
export default Title;
|
||||
20
frontend/src/components/templates/QrCodeComponent.tsx
Normal file
20
frontend/src/components/templates/QrCodeComponent.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import QRImage from '@organisms/QRImage';
|
||||
import Title from '@organisms/Title';
|
||||
import QRForm from '@organisms/QRForm';
|
||||
import styles from '@styles/QRComponent.module.scss';
|
||||
|
||||
const QRComponent = () => {
|
||||
return (
|
||||
<div className={styles.qrComponentBody}>
|
||||
<section className={styles.titleSection}>
|
||||
<Title />
|
||||
</section>
|
||||
<section className={styles.qrBody}>
|
||||
<QRImage src="tmp" />
|
||||
<QRForm />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRComponent;
|
||||
10
frontend/src/main.tsx
Normal file
10
frontend/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import App from './App.tsx';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
12
frontend/src/styles/App.module.scss
Normal file
12
frontend/src/styles/App.module.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(1200px 600px at 10% 10%, #321b43, transparent 10%),
|
||||
radial-gradient(900px 400px at 90% 90%, rgba(178, 33, 246, 0.09), transparent 10%),
|
||||
linear-gradient(180deg, #321b43 0%, #120814 100%);
|
||||
}
|
||||
53
frontend/src/styles/QRComponent.module.scss
Normal file
53
frontend/src/styles/QRComponent.module.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
.qrComponentBody {
|
||||
/* Card container */
|
||||
--bg-deep: #321b43; /* deep purple */
|
||||
--accent-1: #6b3c90;
|
||||
--accent-2: #773da6;
|
||||
--accent-3: #b221f6;
|
||||
--white: #ffffff;
|
||||
|
||||
width: 720px;
|
||||
max-width: calc(100% - 48px);
|
||||
border-radius: 28px;
|
||||
padding: 36px 44px;
|
||||
background: linear-gradient(180deg, rgba(50, 27, 67, 0.95) 0%, rgba(18, 10, 20, 0.85) 100%);
|
||||
color: var(--white);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.03);
|
||||
/* subtle colored inner edge using accent with 9% opacity */
|
||||
box-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.6),
|
||||
inset 0 0 0 1px rgba(178, 33, 246, 0.09);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.titleSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.qrBody {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
padding: 20px 0 8px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Responsive: stack on small screens */
|
||||
@media (max-width: 800px) {
|
||||
.qrBody {
|
||||
flex-direction: column-reverse;
|
||||
gap: 20px;
|
||||
}
|
||||
.titleSection {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
71
frontend/src/styles/QRForm.module.scss
Normal file
71
frontend/src/styles/QRForm.module.scss
Normal file
@@ -0,0 +1,71 @@
|
||||
.qrFormBody {
|
||||
width: 320px;
|
||||
font-family:
|
||||
'Inter',
|
||||
system-ui,
|
||||
-apple-system,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
Arial;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.qrFormLabel {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.qrFormInput {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 12px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 18px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.qrFormSelect {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 18px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.qrFormButton {
|
||||
width: 160px;
|
||||
padding: 10px 18px;
|
||||
background: linear-gradient(180deg, #6b3c90 0%, #773da6 60%, #b221f6 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
margin-top: 8px;
|
||||
transition:
|
||||
transform 0.12s ease,
|
||||
box-shadow 0.12s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&.loading {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.qrError {
|
||||
color: crimson;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
47
frontend/src/styles/QRImage.module.scss
Normal file
47
frontend/src/styles/QRImage.module.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
.qrImageContainer {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #ffffff 100%);
|
||||
border-radius: 18px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
|
||||
border: 6px solid #ffffff; /* white frame like design */
|
||||
}
|
||||
|
||||
.qrImageBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 340px;
|
||||
}
|
||||
|
||||
.qrImg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.qrSpan {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.qrButton {
|
||||
margin-top: 18px;
|
||||
padding: 14px 36px;
|
||||
font-size: 16px;
|
||||
background: linear-gradient(180deg, #6b3c90 0%, #773da6 60%, #b221f6 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 6px 14px rgba(178, 33, 246, 0.25);
|
||||
font-weight: 700;
|
||||
}
|
||||
21
frontend/src/styles/title.module.scss
Normal file
21
frontend/src/styles/title.module.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
margin: 0;
|
||||
letter-spacing: 2px;
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 15px;
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
}
|
||||
9
frontend/src/test/setup.ts
Normal file
9
frontend/src/test/setup.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { expect, afterEach } from 'vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import * as matchers from '@testing-library/jest-dom/matchers';
|
||||
|
||||
expect.extend(matchers);
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
19
frontend/src/vite-env.d.ts
vendored
Normal file
19
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.scss' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.sass' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.less' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
Reference in New Issue
Block a user