+
+
${image.filename}
+
+
+
+ ${
+ image.tags.tags
+ .map(tag=>`${tag}`)
+ .join("")
+ }
+
+
+
+
+
+ `;
+
+ gallery.appendChild(card);
+
+}
+
+document
+ .getElementById("searchBtn")
+ .onclick = ()=>{
+
+ loadImages(
+ document.getElementById("searchInput").value
+ );
+
+};
+
+document
+ .getElementById("homeBtn")
+ .onclick = ()=>loadImages();
+
+
+document
+ .getElementById("uploadBtn")
+ .onclick = async ()=>{
+
+ const file =
+ document.getElementById("imageInput").files[0];
+
+ if(!file)
+ return;
+
+ const form = new FormData();
+
+ form.append("file",file);
+
+ const response = await fetch(API+"/images/upload",{
+
+ method:"POST",
+
+ credentials:"include",
+
+ body:form
+
+ });
+
+ if(response.ok){
+
+ alert("Uploaded!");
+
+ loadImages();
+
+ }
+
+ else{
+
+ alert("Upload failed");
+
+ }
+
+};
+
+
+async function login(email,password){
+
+ await fetch(API+"/auth/login",{
+
+ method:"POST",
+
+ credentials:"include",
+
+ headers:{
+
+ "Content-Type":"application/json"
+
+ },
+
+ body:JSON.stringify({
+
+ email,
+
+ password
+
+ })
+
+ });
+
+}
+
+async function register(email,password){
+
+ await fetch(API+"/auth/register",{
+
+ method:"POST",
+
+ headers:{
+
+ "Content-Type":"application/json"
+
+ },
+
+ body:JSON.stringify({
+
+ email,
+
+ password
+
+ })
+
+ });
+
+}
+
+async function logout(){
+
+ await fetch(API+"/auth/logout",{
+
+ method:"POST",
+
+ credentials:"include"
+
+ });
+
+}
+
+loadImages();
\ No newline at end of file
diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html
index 611f67e..6457a04 100644
--- a/src/main/resources/static/index.html
+++ b/src/main/resources/static/index.html
@@ -1,95 +1,92 @@
-
+
-
Tag_Lens
+
-
-
-
Checking session…
+
+ 📷 TagLens
-
-
-
Tag Lens Panel
-
-
-
-
-
-
Register
-
-
-
-
-
-
-
-
-
-
Login
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AI Image Hosting
+
+
+ Upload images. Let AI describe them. Search anything.
+
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/main/resources/static/script.js b/src/main/resources/static/script.js
deleted file mode 100644
index 5a0a661..0000000
--- a/src/main/resources/static/script.js
+++ /dev/null
@@ -1,151 +0,0 @@
-const BASE = '/auth'; // change this if the backend runs on a different port/domain, e.g. http://localhost:8080/auth
-
-function log(message, isError = false) {
- const body = document.getElementById('logBody');
- const line = document.createElement('div');
- line.className = 'log-line';
- const time = new Date().toLocaleTimeString('en-GB');
- line.innerHTML = `
${time}${message}`;
- body.prepend(line);
-}
-
-function showView(view) {
- document.getElementById('loadingView').classList.add('hidden');
- document.getElementById('guestView').classList.add('hidden');
- document.getElementById('userView').classList.add('hidden');
- document.getElementById(view).classList.remove('hidden');
-}
-
-function renderUser(user) {
- document.getElementById('userEmail').textContent = user.email;
- document.getElementById('userId').textContent = user.id;
- document.getElementById('userCreatedAt').textContent = user.created_at || user.createdAt || '—';
- showView('userView');
-}
-
-// Run on every page load: ask the backend if we're logged in, and render the right view.
-async function checkSession() {
- try {
- const res = await fetch(`${BASE}/me`, {
- method: 'GET',
- credentials: 'include'
- });
- if (res.ok) {
- const data = await res.json();
- log(`Session active (${data.email})`);
- renderUser(data);
- } else {
- log('No active session');
- showView('guestView');
- }
- } catch (e) {
- log(`Connection error: ${e.message}`, true);
- showView('guestView');
- }
-}
-
-async function register() {
- const email = document.getElementById('regEmail').value;
- const password = document.getElementById('regPassword').value;
- try {
- const res = await fetch(`${BASE}/register`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email, password })
- });
- if (res.status === 201) {
- log(`Account created (${email})`);
- } else {
- const text = await res.text();
- log(`Registration failed [${res.status}]: ${text}`, true);
- }
- } catch (e) {
- log(`Connection error: ${e.message}`, true);
- }
-}
-
-async function login() {
- const email = document.getElementById('loginEmail').value;
- const password = document.getElementById('loginPassword').value;
- try {
- const res = await fetch(`${BASE}/login`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include', // required so the cookie gets stored and sent
- body: JSON.stringify({ email, password })
- });
- if (res.ok) {
- log(`Login successful (${email})`);
- checkSession(); // refresh view with real user data from /me
- } else {
- const text = await res.text();
- log(`Login failed [${res.status}]: ${text}`, true);
- }
- } catch (e) {
- log(`Connection error: ${e.message}`, true);
- }
-}
-
-async function logout() {
- try {
- const res = await fetch(`${BASE}/logout`, {
- method: 'POST',
- credentials: 'include'
- });
- if (res.ok) {
- log('Logged out successfully');
- showView('guestView');
- } else {
- log(`Logout failed [${res.status}]`, true);
- }
- } catch (e) {
- log(`Connection error: ${e.message}`, true);
- }
-}
-
-// Kick things off as soon as the page loads
-checkSession();
-
-async function uploadImage() {
- const fileInput = document.getElementById('imageFile');
- const file = fileInput.files[0];
-
- if (!file) {
- log('Pick a file first', true);
- return;
- }
-
- if (file.size > 10 * 1024 * 1024) {
- log('File too large — max 10MB', true);
- return;
- }
-
- const formData = new FormData();
- formData.append('file', file);
-
- try {
- const res = await fetch('/images/upload', {
- method: 'POST',
- credentials: 'include',
- body: formData
- // no Content-Type header — the browser sets the multipart boundary itself
- });
-
- if (res.ok) {
- const data = await res.json();
- log(`Image uploaded (id: ${data.id || 'ok'})`);
-
- // Show a quick local preview of what was just uploaded
- const preview = document.getElementById('uploadPreview');
- preview.innerHTML = '';
- const img = document.createElement('img');
- img.src = URL.createObjectURL(file);
- preview.appendChild(img);
- } else {
- const text = await res.text();
- log(`Upload failed [${res.status}]: ${text}`, true);
- }
- } catch (e) {
- log(`Connection error: ${e.message}`, true);
- }
-}
\ No newline at end of file
diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css
index 36982d2..32aaff7 100644
--- a/src/main/resources/static/style.css
+++ b/src/main/resources/static/style.css
@@ -1,229 +1,173 @@
-:root {
- --bg: #0d1117;
- --panel: #161b22;
- --border: #30363d;
- --text: #e6edf3;
- --muted: #8b949e;
- --accent: #3fb950;
- --accent-dim: #2ea043;
- --danger: #f85149;
- --mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
-}
-
-* { box-sizing: border-box; }
+*{
+ margin:0;
+ padding:0;
+ box-sizing:border-box;
+}
-body {
- margin: 0;
- min-height: 100vh;
- background: var(--bg);
- color: var(--text);
- font-family: var(--mono);
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 24px;
-}
-
-.app {
- width: 100%;
- max-width: 480px;
-}
-
-.title {
- font-size: 13px;
- color: var(--muted);
- letter-spacing: 0.08em;
- text-transform: uppercase;
- margin-bottom: 8px;
-}
-
-.title span { color: var(--accent); }
+body{
-h1 {
- font-size: 22px;
- margin: 0 0 24px;
- font-weight: 600;
-}
+ font-family:Arial,Helvetica,sans-serif;
+ background:#f4f7fb;
+ color:#222;
+}
-.status {
- display: flex;
- align-items: center;
- gap: 8px;
- background: var(--panel);
- border: 1px solid var(--border);
- border-radius: 8px;
- padding: 14px 16px;
- margin-bottom: 20px;
- font-size: 13px;
-}
+header{
-.dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: var(--danger);
- flex-shrink: 0;
- transition: background 0.2s;
-}
-
-.dot.on { background: var(--accent); }
-
-.status-text { color: var(--muted); }
-.status-text b { color: var(--text); }
-
-.panel {
- background: var(--panel);
- border: 1px solid var(--border);
- border-radius: 8px;
- padding: 20px;
- margin-bottom: 16px;
-}
+ display:flex;
+ justify-content:space-between;
+ align-items:center;
+
+ padding:20px 40px;
+
+ background:white;
-.panel-label {
- font-size: 11px;
- color: var(--muted);
- text-transform: uppercase;
- letter-spacing: 0.06em;
- margin-bottom: 14px;
+ box-shadow:0 2px 10px rgba(0,0,0,.08);
}
-label {
- display: block;
- font-size: 12px;
- color: var(--muted);
- margin-bottom: 6px;
+.logo{
+
+ font-size:26px;
+ font-weight:bold;
}
-input {
- width: 100%;
- background: #0d1117;
- border: 1px solid var(--border);
- border-radius: 6px;
- color: var(--text);
- font-family: var(--mono);
- font-size: 13px;
- padding: 10px 12px;
- margin-bottom: 14px;
- outline: none;
- transition: border-color 0.15s;
+nav{
+
+ display:flex;
+ gap:12px;
}
-input:focus { border-color: var(--accent-dim); }
+button{
-.row { display: flex; gap: 10px; }
+ cursor:pointer;
-button {
- font-family: var(--mono);
- font-size: 13px;
- font-weight: 600;
- border: none;
- border-radius: 6px;
- padding: 10px 16px;
- cursor: pointer;
- transition: opacity 0.15s;
-}
+ border:none;
+
+ border-radius:8px;
+
+ padding:10px 18px;
+
+ background:#4b6fff;
-button:hover { opacity: 0.85; }
-button:active { opacity: 0.7; }
+ color:white;
-.btn-primary {
- background: var(--accent-dim);
- color: #0d1117;
- flex: 1;
+ transition:.25s;
}
-.btn-secondary {
- background: transparent;
- color: var(--text);
- border: 1px solid var(--border);
- flex: 1;
+button:hover{
+
+ background:#3157ef;
}
-.btn-danger {
- background: transparent;
- color: var(--danger);
- border: 1px solid var(--danger);
- width: 100%;
+.hero{
+
+ text-align:center;
+
+ margin:40px;
}
-.log {
- background: var(--panel);
- border: 1px solid var(--border);
- border-radius: 8px;
- padding: 14px 16px;
- font-size: 12px;
- color: var(--muted);
- max-height: 160px;
- overflow-y: auto;
+.hero h1{
+
+ font-size:42px;
+
+ margin-bottom:12px;
}
-.log-label {
- font-size: 11px;
- color: var(--muted);
- text-transform: uppercase;
- letter-spacing: 0.06em;
- margin-bottom: 10px;
+.search{
+
+ display:flex;
+
+ justify-content:center;
+
+ gap:10px;
+
+ margin-bottom:25px;
}
-.log-line {
- padding: 4px 0;
- border-bottom: 1px solid #21262d;
- display: flex;
- gap: 8px;
+.search input{
+
+ width:400px;
+
+ padding:12px;
+
+ border-radius:8px;
+
+ border:1px solid #ccc;
}
-.log-line:last-child { border-bottom: none; }
+.upload{
+
+ display:flex;
-.log-line .ok { color: var(--accent); }
-.log-line .err { color: var(--danger); }
-.log-line .time { color: #484f58; flex-shrink: 0; }
+ justify-content:center;
-.hidden { display: none; }
+ gap:15px;
-.center-text {
- text-align: center;
- color: var(--muted);
+ margin-bottom:35px;
}
-.field {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 10px 0;
- border-bottom: 1px solid #21262d;
+.gallery{
+
+ display:grid;
+
+ grid-template-columns:repeat(auto-fill,minmax(280px,1fr));
+
+ gap:25px;
+
+ padding:30px;
}
-.field:last-child { border-bottom: none; }
+.card{
-.field-label {
- font-size: 12px;
- color: var(--muted);
+ background:white;
+
+ border-radius:12px;
+
+ overflow:hidden;
+
+ box-shadow:0 8px 20px rgba(0,0,0,.08);
+
+ transition:.2s;
}
-.field-value {
- font-size: 13px;
- color: var(--text);
- font-weight: 600;
+.card:hover{
+
+ transform:translateY(-4px);
}
-.mono-small {
- font-size: 11px;
- font-weight: 400;
- color: var(--muted);
+.card img{
+
+ width:100%;
+
+ height:220px;
+
+ object-fit:cover;
}
-input[type="file"] {
- padding: 8px;
- font-size: 12px;
+.content{
+
+ padding:18px;
}
-#uploadPreview {
- margin-top: 14px;
+.tags{
+
+ display:flex;
+
+ flex-wrap:wrap;
+
+ gap:8px;
+
+ margin-top:10px;
}
-#uploadPreview img {
- width: 100%;
- border-radius: 6px;
- border: 1px solid var(--border);
- display: block;
+.tag{
+
+ background:#edf2ff;
+
+ color:#3157ef;
+
+ border-radius:20px;
+
+ padding:5px 12px;
+
+ font-size:13px;
}
\ No newline at end of file