Adding the skeleton QR code

This commit is contained in:
2026-03-14 00:24:58 -04:00
commit 6ce0fc0768
27 changed files with 7628 additions and 0 deletions

12
frontend/src/App.tsx Normal file
View 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;

View 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;

View 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;

View 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;

View 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
View 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>
);

View 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%);
}

View 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;
}
}

View 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;
}

View 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;
}

View 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;
}
}

View 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
View 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;
}