@fuzionx/framework 0.1.46 β 0.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -2
- package/cli/index.js +37 -8
- package/cli/templates/make/app-spa/controllers/AuthController.js +114 -0
- package/cli/templates/make/app-spa/controllers/HomeController.js +66 -0
- package/cli/templates/make/app-spa/controllers/PostController.js +191 -0
- package/cli/templates/make/app-spa/controllers/UserController.js +43 -0
- package/cli/templates/make/app-spa/public/css/style.css +1011 -0
- package/cli/templates/make/app-spa/routes/api.js +31 -0
- package/cli/templates/make/app-spa/routes/web.js +19 -0
- package/cli/templates/make/app-spa/services/AuthService.js +48 -0
- package/cli/templates/make/app-spa/services/PostService.js +372 -0
- package/cli/templates/make/app-spa/services/UserService.js +48 -0
- package/cli/templates/make/app-spa/views/default/errors/404.html +11 -0
- package/cli/templates/make/app-spa/views/default/errors/500.html +11 -0
- package/cli/templates/make/app-spa/views/default/layouts/main.html +34 -0
- package/cli/templates/make/app-spa/views/default/pages/home.html +22 -0
- package/cli/templates/make/app-spa/views/default/spa/index.html +13 -0
- package/cli/templates/make/app-spa/views/default/spa/package.json +20 -0
- package/cli/templates/make/app-spa/views/default/spa/src/App.vue +41 -0
- package/cli/templates/make/app-spa/views/default/spa/src/assets/landing.css +220 -0
- package/cli/templates/make/app-spa/views/default/spa/src/assets/style.css +1156 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/AlertDialog.vue +179 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/CodeBlock.vue +33 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/EditorToolbar.vue +54 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/FileUpload.vue +161 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/FlashMessage.vue +39 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/LanguageSwitcher.vue +108 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Lightbox.vue +62 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Navbar.vue +68 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/Pagination.vue +166 -0
- package/cli/templates/make/app-spa/views/default/spa/src/components/ToastContainer.vue +135 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useApi.js +129 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useClipboard.js +44 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useDate.js +73 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useDebounce.js +59 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useFlash.js +46 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useHeartbeat.js +45 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useLocalStorage.js +43 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useLocale.js +79 -0
- package/cli/templates/make/app-spa/views/default/spa/src/composables/useWebSocket.js +93 -0
- package/cli/templates/make/app-spa/views/default/spa/src/main.js +106 -0
- package/cli/templates/make/app-spa/views/default/spa/src/plugins/alert.js +96 -0
- package/cli/templates/make/app-spa/views/default/spa/src/plugins/toast.js +79 -0
- package/cli/templates/make/app-spa/views/default/spa/src/router/index.js +29 -0
- package/cli/templates/make/app-spa/views/default/spa/src/stores/auth.js +58 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardDetail.vue +169 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardForm.vue +192 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/BoardList.vue +129 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/ChatView.vue +317 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/FeaturesView.vue +242 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/HomeView.vue +215 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Login.vue +82 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Profile.vue +85 -0
- package/cli/templates/make/app-spa/views/default/spa/src/views/Register.vue +84 -0
- package/cli/templates/make/app-spa/views/default/spa/vite.config.js +28 -0
- package/cli/templates/make/app-spa/views/default/spa/yarn.lock +633 -0
- package/cli/templates/make/app-spa/ws/ChatHandler.js +138 -0
- package/cli/templates/make/app-ssr/controllers/AuthController.js +119 -0
- package/cli/templates/make/app-ssr/controllers/ChatController.js +15 -0
- package/cli/templates/make/app-ssr/controllers/FeaturesController.js +15 -0
- package/cli/templates/make/app-ssr/controllers/HomeController.js +21 -0
- package/cli/templates/make/app-ssr/controllers/PostController.js +214 -0
- package/cli/templates/make/app-ssr/controllers/UserController.js +48 -0
- package/cli/templates/make/app-ssr/public/css/fx-ui.css +43 -0
- package/cli/templates/make/app-ssr/public/css/landing.css +220 -0
- package/cli/templates/make/app-ssr/public/css/style.css +1011 -0
- package/cli/templates/make/app-ssr/public/js/fx-client.js +107 -0
- package/cli/templates/make/app-ssr/public/js/fx-ui.js +124 -0
- package/cli/templates/make/app-ssr/routes/web.js +46 -0
- package/cli/templates/make/app-ssr/services/AuthService.js +48 -0
- package/cli/templates/make/app-ssr/services/PostService.js +372 -0
- package/cli/templates/make/app-ssr/services/UserService.js +48 -0
- package/cli/templates/make/app-ssr/views/default/errors/404.html +11 -0
- package/cli/templates/make/app-ssr/views/default/errors/500.html +48 -0
- package/cli/templates/make/app-ssr/views/default/layouts/main.html +96 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/form.html +240 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/index.html +73 -0
- package/cli/templates/make/app-ssr/views/default/pages/board/show.html +148 -0
- package/cli/templates/make/app-ssr/views/default/pages/chat.html +288 -0
- package/cli/templates/make/app-ssr/views/default/pages/features.html +373 -0
- package/cli/templates/make/app-ssr/views/default/pages/home.html +258 -0
- package/cli/templates/make/app-ssr/views/default/pages/login.html +27 -0
- package/cli/templates/make/app-ssr/views/default/pages/profile.html +36 -0
- package/cli/templates/make/app-ssr/views/default/pages/register.html +35 -0
- package/cli/templates/make/app-ssr/views/default/partials/pagination.html +75 -0
- package/cli/templates/make/app-ssr/ws/ChatHandler.js +138 -0
- package/package.json +2 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Service } from '@fuzionx/framework';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UserService β SSR μ¬μ©μ μλΉμ€
|
|
5
|
+
*
|
|
6
|
+
* μ¬μ©μ νλ‘ν μ‘°ν λ° μμ λΉμ¦λμ€ λ‘μ§ μ²λ¦¬.
|
|
7
|
+
* λΉλ°λ²νΈ λ³κ²½ μ bcrypt ν΄μ μ μ©.
|
|
8
|
+
*
|
|
9
|
+
* @extends Service
|
|
10
|
+
*/
|
|
11
|
+
export default class UserService extends Service {
|
|
12
|
+
/**
|
|
13
|
+
* μ¬μ©μ λ¨κ±΄ μ‘°ν
|
|
14
|
+
*
|
|
15
|
+
* @param {number} id - μ¬μ©μ ID
|
|
16
|
+
* @returns {Promise<import('../../../database/models/User.js').default|null>} μ¬μ©μ λλ null
|
|
17
|
+
*/
|
|
18
|
+
async find(id) {
|
|
19
|
+
return this.db.User.find(id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* μ¬μ©μ νλ‘ν μμ
|
|
24
|
+
*
|
|
25
|
+
* name, email, password μ€ λ³κ²½λ νλλ§ μ
λ°μ΄νΈ.
|
|
26
|
+
* passwordκ° μμ κ²½μ° bcrypt ν΄μ ν μ μ₯.
|
|
27
|
+
*
|
|
28
|
+
* @param {number} id - μ¬μ©μ ID
|
|
29
|
+
* @param {Object} data - μμ ν λ°μ΄ν°
|
|
30
|
+
* @param {string} [data.name] - μ΄λ¦
|
|
31
|
+
* @param {string} [data.email] - μ΄λ©μΌ
|
|
32
|
+
* @param {string} [data.password] - μ λΉλ°λ²νΈ (νλ¬Έ β bcrypt ν΄μ)
|
|
33
|
+
* @returns {Promise<import('../../../database/models/User.js').default>} μμ λ μ¬μ©μ
|
|
34
|
+
* @throws {AppError} 404 β μ¬μ©μ μμ
|
|
35
|
+
*/
|
|
36
|
+
async update(id, data) {
|
|
37
|
+
const user = await this.db.User.find(id);
|
|
38
|
+
if (!user) throw this.error('User not found', 404);
|
|
39
|
+
|
|
40
|
+
const updates = {};
|
|
41
|
+
if (data.name) updates.name = data.name;
|
|
42
|
+
if (data.email) updates.email = data.email;
|
|
43
|
+
if (data.password) updates.password = this.app.hash.bcrypt(data.password);
|
|
44
|
+
|
|
45
|
+
await user.update(updates);
|
|
46
|
+
return user;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
{% block title %}404{% endblock %}
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="auth-container">
|
|
5
|
+
<div class="glass-card auth-card">
|
|
6
|
+
<div class="auth-logo">404</div>
|
|
7
|
+
<p class="auth-subtitle">{{ t(key="page.404", default="νμ΄μ§λ₯Ό μ°Ύμ μ μμ΅λλ€") }}</p>
|
|
8
|
+
<a href="/" class="btn btn-primary btn-full">{{ t(key="btn.go_home", default="νμΌλ‘") }}</a>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
{% endblock %}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
{% block title %}500{% endblock %}
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="auth-container">
|
|
5
|
+
<div class="glass-card auth-card">
|
|
6
|
+
<div class="auth-logo">500</div>
|
|
7
|
+
<p class="auth-subtitle">{{ t(key="page.500", default="μλ² μ€λ₯κ° λ°μνμ΅λλ€") }}</p>
|
|
8
|
+
|
|
9
|
+
{# κ°λ° λͺ¨λ: μλ¬ λ©μμ§ + μ€ν νΈλ μ΄μ€ νμ #}
|
|
10
|
+
{% if config.debug and error %}
|
|
11
|
+
<div class="error-detail">
|
|
12
|
+
<p class="error-message">{{ error.message }}</p>
|
|
13
|
+
{% if error.stack %}
|
|
14
|
+
<pre class="error-stack">{{ error.stack }}</pre>
|
|
15
|
+
{% endif %}
|
|
16
|
+
</div>
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
19
|
+
<a href="/" class="btn btn-primary btn-full">{{ t(key="btn.go_home", default="νμΌλ‘") }}</a>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
.error-detail {
|
|
25
|
+
margin: 1.5rem 0;
|
|
26
|
+
text-align: left;
|
|
27
|
+
}
|
|
28
|
+
.error-message {
|
|
29
|
+
color: var(--danger, #e74c3c);
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
margin-bottom: 0.75rem;
|
|
32
|
+
word-break: break-all;
|
|
33
|
+
}
|
|
34
|
+
.error-stack {
|
|
35
|
+
background: rgba(0, 0, 0, 0.3);
|
|
36
|
+
color: var(--text-secondary, #aaa);
|
|
37
|
+
padding: 1rem;
|
|
38
|
+
border-radius: var(--radius-sm, 8px);
|
|
39
|
+
font-size: 0.75rem;
|
|
40
|
+
line-height: 1.6;
|
|
41
|
+
overflow-x: auto;
|
|
42
|
+
white-space: pre-wrap;
|
|
43
|
+
word-break: break-all;
|
|
44
|
+
max-height: 400px;
|
|
45
|
+
overflow-y: auto;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
48
|
+
{% endblock %}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="{{ locale }}">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{% block title %}FuzionX{% endblock %}</title>
|
|
7
|
+
<meta name="description" content="{% block description %}FuzionX β Rust-powered full-stack Node.js framework for production-grade applications{% endblock %}">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
10
|
+
<link rel="stylesheet" href="/public/css/style.css">
|
|
11
|
+
<link rel="stylesheet" href="/public/css/landing.css">
|
|
12
|
+
<link rel="stylesheet" href="/public/css/fx-ui.css">
|
|
13
|
+
{% block head %}{% endblock %}
|
|
14
|
+
<script>
|
|
15
|
+
window.__FX__ENC_ENABLED = {{ config.bridge.asp.enabled | default(value=false) }} || {{ config.app.asp.enabled | default(value=false) }}
|
|
16
|
+
</script>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<div class="orb orb-1"></div>
|
|
20
|
+
<div class="orb orb-2"></div>
|
|
21
|
+
<div class="orb orb-3"></div>
|
|
22
|
+
|
|
23
|
+
<nav class="navbar">
|
|
24
|
+
<div class="nav-container">
|
|
25
|
+
<a href="/" class="nav-brand">FuzionX</a>
|
|
26
|
+
<div class="nav-links">
|
|
27
|
+
<a href="/" class="nav-link">{{ t(key="nav.home", default="Home") }}</a>
|
|
28
|
+
<a href="/features" class="nav-link">{{ t(key="nav.features", default="Features") }}</a>
|
|
29
|
+
<a href="/chat" class="nav-link">{{ t(key="nav.chat", default="Chat Demo") }}</a>
|
|
30
|
+
<a href="/board" class="nav-link">{{ t(key="nav.board", default="κ²μν") }}</a>
|
|
31
|
+
{% if auth.user %}
|
|
32
|
+
<a href="/profile" class="nav-link">{{ t(key="nav.profile", default="νλ‘ν") }}</a>
|
|
33
|
+
<form action="/logout" method="POST" class="nav-logout">
|
|
34
|
+
<button type="submit" class="nav-link btn-link">{{ t(key="nav.logout", default="λ‘κ·Έμμ") }}</button>
|
|
35
|
+
</form>
|
|
36
|
+
{% else %}
|
|
37
|
+
<a href="/login" class="nav-link">{{ t(key="nav.login", default="λ‘κ·ΈμΈ") }}</a>
|
|
38
|
+
<a href="/register" class="nav-link">{{ t(key="nav.register", default="νμκ°μ
") }}</a>
|
|
39
|
+
{% endif %}
|
|
40
|
+
<div class="lang-switcher">
|
|
41
|
+
<button class="lang-switcher-btn" onclick="this.parentElement.classList.toggle('open')">
|
|
42
|
+
{{ locale | upper }}
|
|
43
|
+
<svg width="10" height="6" viewBox="0 0 10 6" fill="currentColor"><path d="M1 1l4 4 4-4"/></svg>
|
|
44
|
+
</button>
|
|
45
|
+
<ul class="lang-switcher-menu">
|
|
46
|
+
{% for loc in locales %}
|
|
47
|
+
<li class="lang-switcher-item{% if loc == locale %} active{% endif %}">
|
|
48
|
+
<a href="?lang={{ loc }}" class="lang-switcher-link">{{ loc | upper }}</a>
|
|
49
|
+
</li>
|
|
50
|
+
{% endfor %}
|
|
51
|
+
</ul>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<button class="nav-mobile-toggle" onclick="var nl=document.querySelector('.nav-links'),bd=document.querySelector('.nav-backdrop');nl.classList.toggle('open');bd.classList.toggle('open')" aria-label="Menu">
|
|
55
|
+
<span></span><span></span><span></span>
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
</nav>
|
|
59
|
+
<div class="nav-backdrop" onclick="document.querySelector('.nav-links').classList.remove('open');this.classList.remove('open')"></div>
|
|
60
|
+
|
|
61
|
+
<main class="main-content has-nav">
|
|
62
|
+
{% block flash_messages %}
|
|
63
|
+
{% if flash.error %}
|
|
64
|
+
<div class="container" style="margin-bottom: -1rem;">
|
|
65
|
+
<div class="alert alert-error">{{ flash.error }}</div>
|
|
66
|
+
</div>
|
|
67
|
+
{% endif %}
|
|
68
|
+
{% if flash.success %}
|
|
69
|
+
<div class="container" style="margin-bottom: -1rem;">
|
|
70
|
+
<div class="alert alert-success">{{ flash.success }}</div>
|
|
71
|
+
</div>
|
|
72
|
+
{% endif %}
|
|
73
|
+
{% endblock flash_messages %}
|
|
74
|
+
{% block content %}{% endblock %}
|
|
75
|
+
</main>
|
|
76
|
+
|
|
77
|
+
<script src="/public/js/fx-client.js"></script>
|
|
78
|
+
{% if auth.user and _fx_client_secret %}
|
|
79
|
+
<script>
|
|
80
|
+
FxClient.init('{{ _fx_client_secret | safe }}', '{{ _fx_asp_secret | safe }}', '{{ _fx_asp_header | safe }}');
|
|
81
|
+
setInterval(function() {
|
|
82
|
+
aspFetch('/api/heartbeat')
|
|
83
|
+
.then(function(r) { if (r && r.status === 401) window.location.href = '/login'; })
|
|
84
|
+
.catch(function() {});
|
|
85
|
+
}, 5 * 60 * 1000);
|
|
86
|
+
</script>
|
|
87
|
+
{% elif config.bridge.asp.enabled is defined and config.bridge.asp.enabled %}
|
|
88
|
+
<script>
|
|
89
|
+
FxClient.initPublic('{{ config.bridge.asp.master_secret }}', '{{ config.bridge.asp.header_signal | default(value="Ruxy-Enc-Mode") }}');
|
|
90
|
+
</script>
|
|
91
|
+
{% endif %}
|
|
92
|
+
<script>document.addEventListener('click',function(e){document.querySelectorAll('.lang-switcher.open').forEach(function(el){if(!el.contains(e.target))el.classList.remove('open')})});</script>
|
|
93
|
+
<script src="/public/js/fx-ui.js"></script>
|
|
94
|
+
{% block scripts %}{% endblock %}
|
|
95
|
+
</body>
|
|
96
|
+
</html>
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
{% block title %}{% if post %}{{ t(key="board.edit_title", default="κΈ μμ ") }}{% else %}{{ t(key="board.new_title", default="μ κΈ μμ±") }}{% endif %} β FuzionX{% endblock %}
|
|
3
|
+
{% block flash_messages %}{% endblock %}
|
|
4
|
+
{% block content %}
|
|
5
|
+
<div class="container">
|
|
6
|
+
<div class="glass-card">
|
|
7
|
+
<h1 class="page-title">{% if post %}{{ t(key="board.edit_title", default="κΈ μμ ") }}{% else %}{{ t(key="board.new_title", default="μ κΈ μμ±") }}{% endif %}</h1>
|
|
8
|
+
|
|
9
|
+
<form class="form" action="{% if post %}/board/{{ post.id }}{% else %}/board{% endif %}" method="POST" enctype="multipart/form-data" onsubmit="return handleUpload(event)">
|
|
10
|
+
<div class="form-group">
|
|
11
|
+
<label for="title">{{ t(key="label.title", default="μ λͺ©") }}</label>
|
|
12
|
+
<input type="text" id="title" name="title" value="{% if post %}{{ post.title }}{% endif %}" required>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="form-group">
|
|
16
|
+
<label for="content">{{ t(key="label.content", default="λ΄μ©") }}</label>
|
|
17
|
+
{# ββ λ§ν¬λ€μ΄ μλν° ν΄λ° (νλ μμν¬ κΈ°λ₯ μμ°) ββ #}
|
|
18
|
+
<div class="editor-toolbar" id="editorToolbar">
|
|
19
|
+
<button type="button" onclick="editorCmd('bold')" title="Bold"><b>B</b></button>
|
|
20
|
+
<button type="button" onclick="editorCmd('italic')" title="Italic"><i>I</i></button>
|
|
21
|
+
<button type="button" onclick="editorCmd('underline')" title="Underline"><u>U</u></button>
|
|
22
|
+
<div class="separator"></div>
|
|
23
|
+
<button type="button" onclick="editorInsert('# ')" title="H1">H1</button>
|
|
24
|
+
<button type="button" onclick="editorInsert('## ')" title="H2">H2</button>
|
|
25
|
+
<div class="separator"></div>
|
|
26
|
+
<button type="button" onclick="editorInsert('- ')" title="List">β</button>
|
|
27
|
+
<button type="button" onclick="editorInsert('> ')" title="Quote">β</button>
|
|
28
|
+
<button type="button" onclick="editorInsert('```\n\n```')" title="Code">β¨/β©</button>
|
|
29
|
+
<div class="separator"></div>
|
|
30
|
+
<button type="button" onclick="editorInsert('[λ§ν¬](url)')" title="Link">π</button>
|
|
31
|
+
</div>
|
|
32
|
+
<textarea id="content" name="content" rows="14" class="has-toolbar" required>{% if post %}{{ post.content }}{% endif %}</textarea>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{# ββ κΈ°μ‘΄ 첨λΆνμΌ λͺ©λ‘ (μμ λͺ¨λ, μΈλ€μΌ + κ°λ³ μμ ) ββ #}
|
|
36
|
+
{% if post and files and files | length > 0 %}
|
|
37
|
+
<div class="form-group">
|
|
38
|
+
<label>{{ t(key="board.attached_files", default="κΈ°μ‘΄ 첨λΆνμΌ") }}</label>
|
|
39
|
+
<div class="existing-files-grid">
|
|
40
|
+
{% for file in files %}
|
|
41
|
+
<div class="existing-file-card">
|
|
42
|
+
<div class="existing-file-thumb">
|
|
43
|
+
{% if file.thumbUrl %}
|
|
44
|
+
<img src="{{ file.thumbUrl }}" alt="{{ file.original_name }}" loading="lazy">
|
|
45
|
+
{% elif file.isImage %}
|
|
46
|
+
<img src="{{ file.url }}" alt="{{ file.original_name }}" loading="lazy">
|
|
47
|
+
{% elif file.isVideo %}
|
|
48
|
+
<div class="thumb-placeholder">π¬</div>
|
|
49
|
+
{% else %}
|
|
50
|
+
<div class="thumb-placeholder">π</div>
|
|
51
|
+
{% endif %}
|
|
52
|
+
</div>
|
|
53
|
+
<div class="existing-file-info">
|
|
54
|
+
<span class="file-name" title="{{ file.original_name }}">{{ file.original_name }}</span>
|
|
55
|
+
<button type="button" class="file-remove" title="μμ "
|
|
56
|
+
onclick="if(confirm('μμ ?')){var f=document.createElement('form');f.method='POST';f.action='/board/attachment/{{ file.id }}/delete';document.body.appendChild(f);f.submit();}">β</button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
{% endfor %}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
{% endif %}
|
|
63
|
+
|
|
64
|
+
{# ββ μ νμΌ μ
λ‘λ (λλκ·Έ&λλ‘ + ν΄λ¦) ββ #}
|
|
65
|
+
<div class="form-group">
|
|
66
|
+
<label>{{ t(key="label.attachments", default="첨λΆνμΌ") }}</label>
|
|
67
|
+
<div class="file-upload-area" id="dropZone" onclick="document.getElementById('fileInput').click()">
|
|
68
|
+
<div class="upload-icon">π</div>
|
|
69
|
+
<div class="upload-text">{{ t(key="board.drop_files", default="ν΄λ¦ λλ νμΌμ μ¬κΈ°μ λλκ·Έ") }}</div>
|
|
70
|
+
<div class="upload-hint">{{ t(key="board.file_hint", default="μ΄λ―Έμ§, λΉλμ€, λ¬Έμ νμΌ") }}</div>
|
|
71
|
+
</div>
|
|
72
|
+
<input type="file" id="fileInput" name="files" multiple style="display:none"
|
|
73
|
+
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.zip">
|
|
74
|
+
<div class="file-list" id="fileList"></div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{# ββ μ
λ‘λ μ§νλ₯ ββ #}
|
|
78
|
+
<div class="upload-progress" id="uploadProgress" style="display:none;margin:1rem 0">
|
|
79
|
+
<div class="progress-bar-track" style="background:rgba(255,255,255,0.15);border-radius:8px;height:14px;overflow:hidden">
|
|
80
|
+
<div class="progress-bar-fill" id="progressFill" style="height:100%;width:0;background:linear-gradient(90deg,#667eea,#764ba2);border-radius:8px;transition:width 0.3s ease"></div>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="progress-info" style="display:flex;justify-content:space-between;margin-top:6px;font-size:0.85rem;color:#aaa">
|
|
83
|
+
<span id="progressText">0%</span>
|
|
84
|
+
<span id="progressSpeed"></span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{% if flash.error %}
|
|
89
|
+
<div class="alert alert-error">{{ flash.error }}</div>
|
|
90
|
+
{% endif %}
|
|
91
|
+
{% if flash.success %}
|
|
92
|
+
<div class="alert alert-success">{{ flash.success }}</div>
|
|
93
|
+
{% endif %}
|
|
94
|
+
|
|
95
|
+
<div class="form-actions">
|
|
96
|
+
<button type="submit" class="btn btn-primary" id="submitBtn">{% if post %}{{ t(key="btn.edit", default="μμ ") }}{% else %}{{ t(key="btn.create", default="μμ±") }}{% endif %}</button>
|
|
97
|
+
<a href="/board" class="btn btn-outline">{{ t(key="btn.cancel", default="μ·¨μ") }}</a>
|
|
98
|
+
</div>
|
|
99
|
+
</form>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<script>
|
|
104
|
+
/** μλν° ν
μ€νΈ κ°μΈκΈ° (Bold/Italic/Underline) */
|
|
105
|
+
function editorCmd(type) {
|
|
106
|
+
var ta = document.getElementById('content');
|
|
107
|
+
var start = ta.selectionStart, end = ta.selectionEnd;
|
|
108
|
+
var sel = ta.value.substring(start, end);
|
|
109
|
+
var wrap = { bold: ['**','**'], italic: ['_','_'], underline: ['__','__'] };
|
|
110
|
+
var w = wrap[type] || ['',''];
|
|
111
|
+
ta.setRangeText(w[0] + sel + w[1], start, end, 'end');
|
|
112
|
+
ta.focus();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** μλν° ν
μ€νΈ μ½μ
*/
|
|
116
|
+
function editorInsert(text) {
|
|
117
|
+
var ta = document.getElementById('content');
|
|
118
|
+
ta.setRangeText(text, ta.selectionStart, ta.selectionStart, 'end');
|
|
119
|
+
ta.focus();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** νμΌ μ
λ‘λ β λλκ·Έ&λλ‘ + 미리보기 */
|
|
123
|
+
(function() {
|
|
124
|
+
var drop = document.getElementById('dropZone');
|
|
125
|
+
var input = document.getElementById('fileInput');
|
|
126
|
+
var list = document.getElementById('fileList');
|
|
127
|
+
|
|
128
|
+
function formatSize(bytes) {
|
|
129
|
+
if (bytes < 1024) return bytes + ' B';
|
|
130
|
+
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
131
|
+
return (bytes / 1048576).toFixed(1) + ' MB';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderFiles() {
|
|
135
|
+
list.innerHTML = '';
|
|
136
|
+
if (!input.files.length) return;
|
|
137
|
+
for (var i = 0; i < input.files.length; i++) {
|
|
138
|
+
var f = input.files[i];
|
|
139
|
+
var div = document.createElement('div');
|
|
140
|
+
div.className = 'file-item';
|
|
141
|
+
|
|
142
|
+
var preview = '';
|
|
143
|
+
if (f.type.startsWith('image/')) {
|
|
144
|
+
var url = URL.createObjectURL(f);
|
|
145
|
+
preview = '<img src="' + url + '" class="file-preview">';
|
|
146
|
+
} else if (f.type.startsWith('video/')) {
|
|
147
|
+
preview = '<span class="file-preview-icon">π¬</span>';
|
|
148
|
+
} else {
|
|
149
|
+
preview = '<span class="file-preview-icon">π</span>';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
div.innerHTML = preview + '<span class="file-name">' + f.name + '</span><span class="file-size">' + formatSize(f.size) + '</span>';
|
|
153
|
+
list.appendChild(div);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
input.addEventListener('change', renderFiles);
|
|
158
|
+
|
|
159
|
+
['dragenter','dragover'].forEach(function(e) {
|
|
160
|
+
drop.addEventListener(e, function(ev) { ev.preventDefault(); drop.classList.add('dragover'); });
|
|
161
|
+
});
|
|
162
|
+
['dragleave','drop'].forEach(function(e) {
|
|
163
|
+
drop.addEventListener(e, function(ev) { ev.preventDefault(); drop.classList.remove('dragover'); });
|
|
164
|
+
});
|
|
165
|
+
drop.addEventListener('drop', function(ev) {
|
|
166
|
+
input.files = ev.dataTransfer.files;
|
|
167
|
+
renderFiles();
|
|
168
|
+
});
|
|
169
|
+
})();
|
|
170
|
+
|
|
171
|
+
/** XHR μ
λ‘λ β μ§νλ₯ + μλ 리λ€μ΄λ νΈ */
|
|
172
|
+
function handleUpload(e) {
|
|
173
|
+
var fileInput = document.getElementById('fileInput');
|
|
174
|
+
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
|
|
175
|
+
return true; // νμΌ μμΌλ©΄ κΈ°λ³Έ νΌ μ μΆ
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
var form = e.target;
|
|
180
|
+
var btn = document.getElementById('submitBtn');
|
|
181
|
+
var progress = document.getElementById('uploadProgress');
|
|
182
|
+
var fill = document.getElementById('progressFill');
|
|
183
|
+
var pText = document.getElementById('progressText');
|
|
184
|
+
var pSpeed = document.getElementById('progressSpeed');
|
|
185
|
+
|
|
186
|
+
btn.disabled = true;
|
|
187
|
+
btn.textContent = 'μ
λ‘λ μ€...';
|
|
188
|
+
progress.style.display = 'block';
|
|
189
|
+
|
|
190
|
+
var formData = new FormData(form);
|
|
191
|
+
var xhr = new XMLHttpRequest();
|
|
192
|
+
var startTime = Date.now();
|
|
193
|
+
|
|
194
|
+
xhr.upload.onprogress = function(ev) {
|
|
195
|
+
if (!ev.lengthComputable) return;
|
|
196
|
+
var pct = Math.round((ev.loaded / ev.total) * 100);
|
|
197
|
+
fill.style.width = pct + '%';
|
|
198
|
+
pText.textContent = pct + '%';
|
|
199
|
+
|
|
200
|
+
var elapsed = (Date.now() - startTime) / 1000;
|
|
201
|
+
if (elapsed > 0.5) {
|
|
202
|
+
var bps = ev.loaded / elapsed;
|
|
203
|
+
if (bps > 1048576) {
|
|
204
|
+
pSpeed.textContent = (bps / 1048576).toFixed(1) + ' MB/s';
|
|
205
|
+
} else {
|
|
206
|
+
pSpeed.textContent = (bps / 1024).toFixed(0) + ' KB/s';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
xhr.onload = function() {
|
|
212
|
+
if (xhr.status >= 200 && xhr.status < 400) {
|
|
213
|
+
fill.style.width = '100%';
|
|
214
|
+
pText.textContent = 'μ
λ‘λ μλ£!';
|
|
215
|
+
pSpeed.textContent = '';
|
|
216
|
+
btn.textContent = 'π μΈλ€μΌ μΆμΆμ€...';
|
|
217
|
+
setTimeout(function() {
|
|
218
|
+
window.location.href = xhr.responseURL || '/board';
|
|
219
|
+
}, 1500);
|
|
220
|
+
} else {
|
|
221
|
+
btn.disabled = false;
|
|
222
|
+
btn.textContent = 'μ¬μλ';
|
|
223
|
+
pText.textContent = 'μ€λ₯ λ°μ';
|
|
224
|
+
fill.style.background = '#e74c3c';
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
xhr.onerror = function() {
|
|
229
|
+
btn.disabled = false;
|
|
230
|
+
btn.textContent = 'μ¬μλ';
|
|
231
|
+
pText.textContent = 'λ€νΈμν¬ μ€λ₯';
|
|
232
|
+
fill.style.background = '#e74c3c';
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
xhr.open('POST', form.action);
|
|
236
|
+
xhr.send(formData);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
</script>
|
|
240
|
+
{% endblock %}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
{% block title %}{{ t(key="board.title", default="κ²μν") }} β FuzionX{% endblock %}
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="container">
|
|
5
|
+
<div class="glass-card">
|
|
6
|
+
<div class="page-header">
|
|
7
|
+
<h1 class="page-title">{{ t(key="board.title", default="κ²μν") }}</h1>
|
|
8
|
+
<a href="/board/new" class="btn btn-primary">{{ t(key="btn.new_post", default="μ κΈ") }}</a>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="post-list">
|
|
12
|
+
{% for post in posts %}
|
|
13
|
+
<a href="/board/{{ post.id }}" class="post-card">
|
|
14
|
+
{% if post.thumbUrl %}
|
|
15
|
+
<div class="post-card-thumb">
|
|
16
|
+
<img src="{{ post.thumbUrl }}" alt="{{ post.title }}" loading="lazy">
|
|
17
|
+
</div>
|
|
18
|
+
{% endif %}
|
|
19
|
+
<div class="post-card-body">
|
|
20
|
+
<h3 class="post-card-title">
|
|
21
|
+
{% if post.status == "processing" %}<span class="badge badge-processing">π {{ t(key="board.processing", default="μ²λ¦¬μ€") }}</span> {% endif %}{{ post.title }}
|
|
22
|
+
</h3>
|
|
23
|
+
<div class="post-card-meta">
|
|
24
|
+
<span>{{ post.user_name | default(value="-") }}</span>
|
|
25
|
+
<span>{{ post.created_at }}</span>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</a>
|
|
29
|
+
{% endfor %}
|
|
30
|
+
{% if not posts or posts | length == 0 %}
|
|
31
|
+
<div class="post-list-empty">{{ t(key="board.empty", default="κ²μκΈμ΄ μμ΅λλ€.") }}</div>
|
|
32
|
+
{% endif %}
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{% set baseUrl = "/board" %}
|
|
36
|
+
{% include "partials/pagination.html" %}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{# processing κ²μκΈμ΄ μμΌλ©΄ REST ν΄λ§μΌλ‘ μν νμΈ #}
|
|
41
|
+
<script>
|
|
42
|
+
(function() {
|
|
43
|
+
var badges = document.querySelectorAll('.badge-processing');
|
|
44
|
+
if (!badges.length) return;
|
|
45
|
+
|
|
46
|
+
// processing post ID μμ§ (λΆλͺ¨ .post-cardμ hrefμμ μΆμΆ)
|
|
47
|
+
var ids = [];
|
|
48
|
+
badges.forEach(function(b) {
|
|
49
|
+
var card = b.closest('.post-card');
|
|
50
|
+
if (card) {
|
|
51
|
+
var m = card.getAttribute('href').match(/\/board\/(\d+)/);
|
|
52
|
+
if (m) ids.push(m[1]);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
if (!ids.length) return;
|
|
56
|
+
|
|
57
|
+
var timer = setInterval(function() {
|
|
58
|
+
aspFetch('/api/board/status?ids=' + ids.join(','))
|
|
59
|
+
.then(function(res) {
|
|
60
|
+
if (!res || !res.statuses) return;
|
|
61
|
+
var allDone = true;
|
|
62
|
+
for (var i = 0; i < ids.length; i++) {
|
|
63
|
+
if (res.statuses[ids[i]] === 'processing') { allDone = false; break; }
|
|
64
|
+
}
|
|
65
|
+
if (allDone) {
|
|
66
|
+
clearInterval(timer);
|
|
67
|
+
location.reload();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}, 5000);
|
|
71
|
+
})();
|
|
72
|
+
</script>
|
|
73
|
+
{% endblock %}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
{% block title %}{{ post.title }} β FuzionX{% endblock %}
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="container">
|
|
5
|
+
<div class="glass-card">
|
|
6
|
+
<div class="page-header">
|
|
7
|
+
<h1 class="page-title">
|
|
8
|
+
{% if post.status == "processing" %}<span class="badge badge-processing">π {{ t(key="board.processing", default="μ²λ¦¬μ€") }}</span> {% endif %}{{ post.title }}
|
|
9
|
+
</h1>
|
|
10
|
+
{% if auth.user.id == post.user_id %}
|
|
11
|
+
<div class="page-actions">
|
|
12
|
+
<a href="/board/{{ post.id }}/edit" class="btn btn-outline">{{ t(key="btn.edit", default="μμ ") }}</a>
|
|
13
|
+
<form action="/board/{{ post.id }}/delete" method="POST" style="display:inline">
|
|
14
|
+
<button type="submit" class="btn btn-danger" onclick="return confirm('{{ t(key="board.confirm_delete", default="μμ νμκ² μ΅λκΉ?") }}')">{{ t(key="btn.delete", default="μμ ") }}</button>
|
|
15
|
+
</form>
|
|
16
|
+
</div>
|
|
17
|
+
{% endif %}
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="post-meta">
|
|
21
|
+
<span>{% if author %}{{ author.name }}{% else %}-{% endif %}</span>
|
|
22
|
+
<span>{{ post.created_at }}</span>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="post-content">{{ post.content }}</div>
|
|
26
|
+
|
|
27
|
+
{# ββ λΉλμ€ μΈλ€μΌ λ³Έλ¬Έ νμ ββ #}
|
|
28
|
+
{% if files and files | length > 0 %}
|
|
29
|
+
{% for file in files %}
|
|
30
|
+
{% if file.isVideo and file.thumbUrl %}
|
|
31
|
+
<div class="inline-video">
|
|
32
|
+
<img src="{{ file.thumbUrl }}" alt="{{ file.original_name }}" style="width:100%;max-height:500px;object-fit:contain;border-radius:var(--radius-sm, 8px)">
|
|
33
|
+
</div>
|
|
34
|
+
{% endif %}
|
|
35
|
+
{% endfor %}
|
|
36
|
+
{% endif %}
|
|
37
|
+
|
|
38
|
+
{# ββ 첨λΆνμΌ μΉμ
ββ #}
|
|
39
|
+
{% if files and files | length > 0 %}
|
|
40
|
+
<div class="post-attachments">
|
|
41
|
+
<h3>π {{ t(key="label.attachments", default="첨λΆνμΌ") }} ({{ files | length }})</h3>
|
|
42
|
+
<div class="attachment-grid">
|
|
43
|
+
{% for file in files %}
|
|
44
|
+
<div class="attachment-card">
|
|
45
|
+
{% if file.isImage %}
|
|
46
|
+
{# μ΄λ―Έμ§: μΈλ€μΌ νμ, ν΄λ¦ μ μλ³Έ lightbox #}
|
|
47
|
+
<a href="{{ file.url }}" class="lightbox-trigger" data-type="image" target="_blank">
|
|
48
|
+
<div class="attachment-thumb">
|
|
49
|
+
<img src="{{ file.thumbUrl | default(value=file.url) }}" alt="{{ file.original_name }}" loading="lazy">
|
|
50
|
+
</div>
|
|
51
|
+
<div class="attachment-info">
|
|
52
|
+
<span class="attachment-name" title="{{ file.original_name }}">πΌοΈ {{ file.original_name }}</span>
|
|
53
|
+
</div>
|
|
54
|
+
</a>
|
|
55
|
+
{% elif file.isVideo %}
|
|
56
|
+
{# λΉλμ€: νλ μ μΈλ€μΌ + βΆ μ€λ²λ μ΄ #}
|
|
57
|
+
<a href="{{ file.url }}" class="lightbox-trigger" data-type="video" target="_blank">
|
|
58
|
+
<div class="attachment-thumb">
|
|
59
|
+
{% if file.thumbUrl %}
|
|
60
|
+
<img src="{{ file.thumbUrl }}" alt="{{ file.original_name }}" loading="lazy">
|
|
61
|
+
{% else %}
|
|
62
|
+
<div class="thumb-placeholder">π¬</div>
|
|
63
|
+
{% endif %}
|
|
64
|
+
<div class="play-overlay">βΆ</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="attachment-info">
|
|
67
|
+
<span class="attachment-name" title="{{ file.original_name }}">π¬ {{ file.original_name }}</span>
|
|
68
|
+
</div>
|
|
69
|
+
</a>
|
|
70
|
+
{% else %}
|
|
71
|
+
{# μΌλ° νμΌ: λ€μ΄λ‘λ λ§ν¬ #}
|
|
72
|
+
<a href="{{ file.url }}" class="attachment-download" download>
|
|
73
|
+
<div class="attachment-thumb">
|
|
74
|
+
<div class="thumb-placeholder">π</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="attachment-info">
|
|
77
|
+
<span class="attachment-name" title="{{ file.original_name }}">π {{ file.original_name }}</span>
|
|
78
|
+
</div>
|
|
79
|
+
</a>
|
|
80
|
+
{% endif %}
|
|
81
|
+
</div>
|
|
82
|
+
{% endfor %}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
{% endif %}
|
|
86
|
+
|
|
87
|
+
<div class="post-footer">
|
|
88
|
+
<a href="/board" class="btn btn-outline">{{ t(key="btn.back", default="β λͺ©λ‘") }}</a>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{# processing μνλ©΄ REST ν΄λ§μΌλ‘ μν νμΈ #}
|
|
94
|
+
{% if post.status == "processing" %}
|
|
95
|
+
<script>
|
|
96
|
+
(function() {
|
|
97
|
+
var postId = '{{ post.id }}';
|
|
98
|
+
var timer = setInterval(function() {
|
|
99
|
+
aspFetch('/api/board/status?ids=' + postId)
|
|
100
|
+
.then(function(res) {
|
|
101
|
+
if (!res || !res.statuses) return;
|
|
102
|
+
if (res.statuses[postId] !== 'processing') {
|
|
103
|
+
clearInterval(timer);
|
|
104
|
+
location.reload();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}, 5000);
|
|
108
|
+
})();
|
|
109
|
+
</script>
|
|
110
|
+
{% endif %}
|
|
111
|
+
|
|
112
|
+
{# ββ Lightbox λͺ¨λ¬ (μ΄λ―Έμ§ μλ³Έ / λΉλμ€ μ¬μ) ββ #}
|
|
113
|
+
<div class="lightbox-overlay" id="lightbox" style="display:none">
|
|
114
|
+
<div class="lightbox-content">
|
|
115
|
+
<button class="lightbox-close" onclick="closeLightbox()">β</button>
|
|
116
|
+
<div id="lightboxBody"></div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<script>
|
|
121
|
+
/** Lightbox β μ΄λ―Έμ§ μλ³Έ 보기 / λΉλμ€ μ¬μ λͺ¨λ¬ */
|
|
122
|
+
document.querySelectorAll('.lightbox-trigger').forEach(function(el) {
|
|
123
|
+
el.addEventListener('click', function(e) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
var url = this.getAttribute('href');
|
|
126
|
+
var type = this.getAttribute('data-type');
|
|
127
|
+
var body = document.getElementById('lightboxBody');
|
|
128
|
+
|
|
129
|
+
if (type === 'video') {
|
|
130
|
+
body.innerHTML = '<video controls autoplay style="max-width:100%;max-height:80vh"><source src="' + url + '"></video>';
|
|
131
|
+
} else {
|
|
132
|
+
body.innerHTML = '<img src="' + url + '" style="max-width:100%;max-height:80vh">';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
document.getElementById('lightbox').style.display = 'flex';
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
function closeLightbox() {
|
|
140
|
+
document.getElementById('lightbox').style.display = 'none';
|
|
141
|
+
document.getElementById('lightboxBody').innerHTML = '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
document.getElementById('lightbox').addEventListener('click', function(e) {
|
|
145
|
+
if (e.target === this) closeLightbox();
|
|
146
|
+
});
|
|
147
|
+
</script>
|
|
148
|
+
{% endblock %}
|