This commit is contained in:
DeSqBlocki
2026-08-15 08:38:49 +02:00
commit bab51615c9
29 changed files with 2729 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules
.env
+25
View File
@@ -0,0 +1,25 @@
const express = require('express');
const app = express();
const cardRouter = require('./routes/card');
const guestbookRouter = require('./routes/guestbook');
const countdownRouter = require('./routes/countdown');
// Set the view engine to EJS
app.set('view engine', 'ejs');
app.use(express.static('public')); // To serve static files (CSS, images, etc.)
// Route for handling cards
app.use('/card', cardRouter);
app.use('/guestbook', guestbookRouter);
app.use('/countdown', countdownRouter);
app.get('*', function(req, res){
res.redirect('/card/en');
});
// Start the server
const port = 3000;
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
+17
View File
@@ -0,0 +1,17 @@
{
"lang": "de",
"menu_card": "Karte",
"menu_guestbook": "Gästebuch",
"menu_countdown": "Countdown",
"title": "Karte",
"description": "Einladung zur Hochzeit von Milenya & Bunbun",
"hover_hint":"Klick hier!",
"front_invation_1": "In den Reichen von Eorzea haben wir uns gefunden und nun, von den Sternen gesegnet, treten wir in den Bund der Ewigkeit.",
"front_invation_2": "Diesen Moment möchten wir mit euch teilen am 28.09.2024 um 15 Uhr im Ostwald am Dom der Einkehr.",
"front_invation_3": "Im Anschluss, wer möchte, kann sich gerne uns anschließen, um in Shirogane, Bezirk 23, Nr. 15 dieses Bündniss zu feiern.",
"back_info": "Bitte beachtet:",
"back_style": "Stil: Formell",
"back_rules": "Keine Minions, Keine Meme Outfits",
"back_afterword": "Bei weiteren Fragen könnt ihr euch gerne jederzeit bei uns melden",
"back_signed": "LG Milenya & Bunbun"
}
+17
View File
@@ -0,0 +1,17 @@
{
"lang": "en",
"menu_card": "Card",
"menu_guestbook": "Guestbook",
"menu_countdown": "Countdown",
"title": "Card",
"description": "Invitation to the wedding of Milenya & Bunbun",
"hover_hint":"Click here!",
"front_invation_1": "In the realms of Eorzea, we have found each other, and now, blessed by the stars, we enter into the ceremony of eternal bonding.",
"front_invation_2": "We would like to share this moment with you on September 28th 2024, at 13 o'clock Server Time in the Sanctum of the Twelve in East Shroud.",
"front_invation_3": "Afterwards, anyone who wishes can gladly join the festivities in Shirogane, Ward 23, Plot 15.",
"back_info": "Please note:",
"back_style": "Style: Formal",
"back_rules": "No Minions, No Meme Outfits",
"back_afterword": "If you have any questions, feel free to reach out to us",
"back_signed": "Kind regards, Milenya & Bunbun"
}
+10
View File
@@ -0,0 +1,10 @@
{
"lang": "de",
"menu_card": "Karte",
"menu_guestbook": "Gästebuch",
"menu_countdown": "Countdown",
"title": "Hochzeit Timer",
"description": "Unser Hochzeit Timer",
"countdown_text": "Hochzeit Timer :",
"ountdown_finished": "Es ist soweit!"
}
+10
View File
@@ -0,0 +1,10 @@
{
"lang": "en",
"menu_card": "Card",
"menu_guestbook": "Guestbook",
"menu_countdown": "Countdown",
"title": "Wedding Timer",
"description": "our wedding timer",
"countdown_text": "Wedding Timer:",
"countdown_finished": "It is time!"
}
+15
View File
@@ -0,0 +1,15 @@
{
"lang": "de",
"menu_card": "Karte",
"menu_guestbook": "Gästebuch",
"menu_countdown": "Countdown",
"title": "Gästebuch",
"description": "Unser Gästebuch",
"guestbook_welcome": "Willkommen im Gästebuch!",
"guestbook_name": "Dein Name:",
"guestbook_name_placeholder": "Wer bist du?",
"guestbook_comment": "Deine Nachricht:",
"guestbook_comment_placeholder": "Was möchtest du mit uns teilen?",
"guestbook_submit": "Absenden",
"messages_wrote": "schrieb"
}
+15
View File
@@ -0,0 +1,15 @@
{
"lang": "en",
"menu_card": "Card",
"menu_guestbook": "Guestbook",
"menu_countdown": "Countdown",
"title": "Guestbook",
"description": "our guestbook",
"guestbook_welcome": "Welcome to our Guestbook!",
"guestbook_name":"Your Name:",
"guestbook_name_placeholder":"Who are you?",
"guestbook_comment":"Your Message:",
"guestbook_comment_placeholder": "What would you like to share with us?",
"guestbook_submit": "Submit",
"messages_wrote": "wrote"
}
+26
View File
@@ -0,0 +1,26 @@
[
{
"name": "Aquanort",
"comment": "Plenty of silly jokes *wink*",
"imagePath": null,
"originalFilename": null
},
{
"name": "Strange Person",
"comment": "“May your choices reflect your hopes, not your fears.” ",
"imagePath": null,
"originalFilename": null
},
{
"name": "Dorythos Tuulai",
"comment": "Freue mich schon! ",
"imagePath": null,
"originalFilename": null
},
{
"name": "Milaysa Federschein",
"comment": "May your Hearts and Souls always beat in the same Rhythm ♥♥♥",
"imagePath": "/upload/Screenshot 2024-09-25 181537-1727281135769-e31074a6.png",
"originalFilename": "Screenshot 2024-09-25 181537.png"
}
]
+1117
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "milenbun",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bootstrap": "^5.3.3",
"crypto": "^1.0.1",
"ejs": "^3.1.10",
"express": "^4.21.0",
"jquery": "^3.7.1",
"multer": "^1.4.5-lts.1",
"path": "^0.12.7"
}
}
+268
View File
@@ -0,0 +1,268 @@
/* Basic styling for the entire page */
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: 'Great Vibes', cursive; /* Use Great Vibes font */
background-image: url('../images/background.gif');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
backdrop-filter: blur(4px);
/* Apply blur effect */
}
/* Invitation card container with 3D perspective */
.invitation-card {
width: 100%;
max-width: 650px;
height: 100%;
max-height: 850px;
perspective: 900px;
/* Creates the 3D effect */
position: relative;
overflow: hidden;
cursor: pointer;
animation: pulse 5s infinite;
}
.invitation-card:hover {
animation: none;
}
/* Inner container for the flip effect */
.card-inner {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.8s ease-in-out;
}
.invitation-card.flipped .card-inner {
transform: rotateY(180deg);
}
/* Styling for the front and back of the card */
.card-front,
.card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 10px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
/* Adds a shadow for a better visual appearance */
}
.card-image {
width: 100%;
height: 100%;
object-fit: cover;
/* Ensures the image covers the entire card */
}
/* Flip the back side of the card */
.card-back {
transform: rotateY(180deg);
}
/* Text on top of the card */
.card-text {
position: absolute;
width: 100%;
height: 100%;
display: flex;
text-align: center;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 20px;
color: rgba(249, 104, 14, 0.671);
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: rgba(0, 0, 0, 0.2);
box-sizing: border-box;
}
.title-text {
position: absolute;
width: 100%;
height: 100%;
text-align: center;
flex-direction: column;
padding: 20px;
display: inline-block; /* This makes the underline match the text width */
color: rgba(249, 104, 14, 0.671);
font-size: 40px;
font-style: italic;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: rgba(0, 0, 0, 0.2);
box-sizing: border-box;
}
h1, h2, p {
margin: 5px;
}
.custom-text-align {
max-width: 80%;
margin: 0 auto;
padding-top: 20%;
padding-left: 20px;
padding-right: 20px;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
p {
font-size: 1.6em;
}
/* Outer glow effect for text */
.card-text h1, .card-text p {
text-shadow: 0 0 10px rgba(170, 103, 2, 0.8);
}
.custom-text-align p {
text-shadow: 0 0 40px rgba(255, 255, 255, 0.8);
}
/* Hover hint styling */
.hover-hint {
position: absolute;
bottom: 20%;
left: 50%;
font-size: 20px;
transform: translateX(-50%);
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 20px 40px;
border-radius: 5px;
opacity: 0; /* Initially hidden */
transition: opacity 0.3s ease;
z-index: 1;
pointer-events: none; /* Avoid interaction when hidden */
}
/* Hover hint disappears on hover */
.invitation-card:hover .hover-hint {
opacity: 0;
pointer-events: none;
}
/* Language switcher styles */
.language-switcher {
position: absolute;
top: 20px;
right: 20px;
}
.language-switcher a {
color: white;
text-decoration: none;
padding: 10px 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
margin: 0 5px;
transition: background-color 0.3s ease;
}
.language-switcher a:hover {
background-color: rgba(0, 0, 0, 0.8);
}
/* Menu styles similar to language switcher */
.menu-btn {
color: white;
background-color: rgba(0, 0, 0, 0.6);
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
position: absolute;
top: 10px;
left: 20px;
}
.menu-btn:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.retractable-menu {
display: none;
position: absolute;
top: 60px;
left: 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
padding: 10px 20px;
}
.retractable-menu.open {
display: block;
}
.retractable-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.retractable-menu ul li {
margin: 10px 0;
}
.retractable-menu ul li a {
color: white;
text-decoration: none;
padding: 10px 20px;
display: block;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
transition: background-color 0.3s ease;
}
.retractable-menu ul li a:hover {
background-color: rgba(0, 0, 0, 0.8);
}
/* Mobile responsiveness */
@media (max-width: 600px) {
body {
height: auto;
padding: 20px;
background-size: cover;
}
.invitation-card {
max-width: 95vw;
max-height: 80vh;
margin: 0 auto;
padding: 10px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
p {
font-size: 1em;
}
}
+313
View File
@@ -0,0 +1,313 @@
/* Basic styling for the entire page */
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: 'Great Vibes', cursive; /* Use Great Vibes font */
background-image: url('../images/background.gif');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
backdrop-filter: blur(4px);
/* Apply blur effect */
}
.invitation-card {
cursor: pointer;
}
.hover-hint {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 10px 20px;
border-radius: 5px;
opacity: 0; /* Initially hidden */
transition: opacity 0.3s ease;
z-index: 1;
pointer-events: none; /* Avoid interaction when hidden */
animation: showHint 0.3s ease forwards; /* Add an animation */
animation-delay: 5s; /* Delay the appearance by 5 seconds */
}
@keyframes showHint {
from {
opacity: 0;
}
to {
opacity: 1; /* Become visible */
}
}
.invitation-card:hover .hover-hint {
opacity: 0;
pointer-events: none;
animation: none; /* Instantly hide when hovered */
}
/* Invitation card container with 3D perspective */
.invitation-card {
width: 100%;
max-width: 650px;
height: 100%;
max-height: 850px;
perspective: 900px;
/* Creates the 3D effect */
position: relative;
overflow: hidden;
}
/* Inner container for the flip effect */
.card-inner {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.8s ease-in-out;
}
/* Flip the card on hover */
.invitation-card:hover .card-inner {
transform: rotateY(180deg);
}
/* Styling for the front and back of the card */
.card-front,
.card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 10px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
/* Adds a shadow for a better visual appearance */
}
.card-image {
width: 100%;
height: 100%;
object-fit: cover;
/* Ensures the image covers the entire card */
}
/* Flip the back side of the card */
.card-back {
transform: rotateY(180deg);
}
/* Text on top of the card */
.card-text {
position: absolute;
width: 100%;
height: 100%;
display: flex;
text-align: center;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 20px;
/* Slight overlay for better readability of text */
color: rgba(249, 104, 14, 0.671);
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: rgba(0, 0, 0, 0.2);
box-sizing: border-box;
}
.title-text {
position: absolute;
width: 100%;
height: 100%;
display: flex;
text-align: center;
flex-direction: column;
padding: 20px;
display: inline-block; /* This makes the underline match the text width */
/* Slight overlay for better readability of text */
color: rgba(249, 104, 14, 0.671);
font-size: 40px;
font-style: italic;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: rgba(0, 0, 0, 0.2);
box-sizing: border-box;
}
h1,
h2,
p {
margin: 5px;
}
.custom-text-align {
max-width: 80%;
/* Set a max width */
margin: 0 auto;
padding-top: 20%;
/* Center the container */
padding-left: 20px;
padding-right: 20px;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
p {
font-size: 1.6em;
}
/* Outer glow effect for text */
.card-text h1,
.card-text p {
text-shadow: 0 0 10px rgba(170, 103, 2, 0.8)
}
.custom-text-align p {
text-shadow: 0 0 40px rgba(255, 255, 255, 0.8);
}
/* Language switcher styles */
.language-switcher {
position: absolute;
top: 20px;
right: 20px;
}
.language-switcher a {
color: white;
text-decoration: none;
padding: 10px 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
margin: 0 5px;
transition: background-color 0.3s ease;
}
.language-switcher a:hover {
background-color: rgba(0, 0, 0, 0.8);
}
/* Menu styles similar to language switcher */
.menu-btn {
color: white;
background-color: rgba(0, 0, 0, 0.6);
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
position: absolute;
top: 10px;
left: 20px;
}
.menu-btn:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.retractable-menu {
display: none;
position: absolute;
top: 60px;
left: 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
padding: 10px 20px;
}
.retractable-menu.open {
display: block;
}
.retractable-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.retractable-menu ul li {
margin: 10px 0;
}
.retractable-menu ul li a {
color: white;
text-decoration: none;
padding: 10px 20px;
display: block;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
transition: background-color 0.3s ease;
}
.retractable-menu ul li a:hover {
background-color: rgba(0, 0, 0, 0.8);
}
/* Mobile responsiveness */
@media (max-width: 600px) {
body {
height: auto;
padding: 20px;
background-size: cover;
}
.invitation-card {
max-width: 95vw;
/* Adjust the card width for mobile devices */
max-height: 80vh;
/* Adjust the card height for mobile devices */
margin: 0 auto;
/* Center the card */
padding: 10px;
/* Add some padding around the card */
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
p {
font-size: 1em;
}
}
/* Pulse animation to the invitation card */
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.invitation-card {
animation: pulse 5s infinite;
overflow: hidden;
}
.invitation-card:hover {
animation: none;
}
+144
View File
@@ -0,0 +1,144 @@
.countdown-card {
background-color: rgba(255, 255, 255, 0.7);
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
padding: 30px;
text-align: center;
width: 600px;
}
.countdown {
font-size: 5em; /* Larger font for visibility */
color: rgba(249, 104, 14, 0.671); /* Color for the countdown */
}
/* Basic styling for the entire page */
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-image: url('../images/background.gif');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
font-family: 'Great Vibes', cursive; /* Use Great Vibes font */
font-style: italic;
backdrop-filter: blur(4px); /* Apply blur effect */
}
.card {
background: rgba(255, 255, 255, 0.8); /* Semi-transparent white background */
}
.card-img-top {
border-radius: 0; /* Remove rounded edges for the image */
}
.btn-primary {
background-color: #007bff; /* Bootstrap primary color */
border: none;
}
.btn-primary:hover {
background-color: #0056b3; /* Darker shade on hover */
}
/* Language switcher styles */
.language-switcher {
position: absolute;
top: 20px;
right: 20px;
}
.language-switcher a {
color: white;
text-decoration: none;
padding: 10px 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
margin: 0 5px;
transition: background-color 0.3s ease;
}
.language-switcher a:hover {
background-color: rgba(0, 0, 0, 0.8);
}
/* Menu styles similar to language switcher */
.menu-button {
color: white;
background-color: rgba(0, 0, 0, 0.6);
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
position: absolute;
top: 10px;
left: 20px;
}
.menu-button:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.retractable-menu {
display: none;
position: absolute;
top: 60px;
left: 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
padding: 10px 20px;
}
.retractable-menu.open {
display: block;
}
.retractable-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.retractable-menu ul li {
margin: 10px 0;
}
.retractable-menu ul li a {
color: white;
text-decoration: none;
padding: 10px 20px;
display: block;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
transition: background-color 0.3s ease;
}
.retractable-menu ul li a:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.countdown-title {
font-size: xx-large;
color: rgb(0, 0, 0);
}
/* Mobile responsiveness */
@media (max-width: 600px) {
body {
height: auto;
padding: 20px;
background-size: cover;
}
.invitation-card {
max-width: 95vw; /* Adjust the card width for mobile devices */
max-height: 80vh; /* Adjust the card height for mobile devices */
margin: 0 auto; /* Center the card */
padding: 10px; /* Add some padding around the card */
}
}
+151
View File
@@ -0,0 +1,151 @@
/* Basic styling for the entire page */
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-image: url('../images/background.gif');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
font-family: 'Great Vibes', cursive; /* Use Great Vibes font */
font-style: italic;
backdrop-filter: blur(4px); /* Apply blur effect */
}
.card {
background: rgba(255, 255, 255, 0.8); /* Semi-transparent white background */
}
.card-img-top {
border-radius: 0; /* Remove rounded edges for the image */
}
.btn-primary {
background-color: #007bff; /* Bootstrap primary color */
border: none;
}
.btn-primary:hover {
background-color: #0056b3; /* Darker shade on hover */
}
/* Language switcher styles */
.language-switcher {
position: absolute;
top: 20px;
right: 20px;
}
.language-switcher a {
color: white;
text-decoration: none;
padding: 10px 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
margin: 0 5px;
transition: background-color 0.3s ease;
}
.language-switcher a:hover {
background-color: rgba(0, 0, 0, 0.8);
}
/* Menu styles similar to language switcher */
.menu-button {
color: white;
background-color: rgba(0, 0, 0, 0.6);
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
position: absolute;
top: 20px;
left: 20px;
}
.menu-button:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.retractable-menu {
display: none;
position: absolute;
top: 60px;
left: 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
padding: 10px 20px;
}
.retractable-menu.open {
display: block;
}
.retractable-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.retractable-menu ul li {
margin: 10px 0;
}
.retractable-menu ul li a {
color: white;
text-decoration: none;
padding: 10px 20px;
display: block;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 5px;
transition: background-color 0.3s ease;
}
.retractable-menu ul li a:hover {
background-color: rgba(0, 0, 0, 0.8);
}
/* Scrollable messages section */
.scrollable-messages {
max-height: 400px; /* Set the maximum height */
overflow-y: auto; /* Enable vertical scroll */
padding: 10px;
background-color: rgba(255, 255, 255, 0.8); /* Same background for consistency */
border-radius: 8px;
}
/* Style for each individual message */
.uploaded-image {
max-width: 100%;
max-height: 200px;
object-fit: cover;
border-radius: 10px;
cursor: pointer;
}
.list-group-item {
margin-bottom: 15px; /* Space between messages */
}
/* Mobile responsiveness */
@media (max-width: 600px) {
body {
height: auto;
padding: 20px;
background-size: cover;
}
.invitation-card {
max-width: 95vw; /* Adjust the card width for mobile devices */
max-height: 80vh; /* Adjust the card height for mobile */
}
}
/* Ensure the modal image scales properly */
.modal-body img {
max-width: 100%;
height: auto;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

+27
View File
@@ -0,0 +1,27 @@
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs');
// Function to load the appropriate language file
function getLanguageFile(language) {
const filePath = path.join(__dirname, `../locales/card/${language}.json`);
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
// Route for the card, with language support
router.get('/:lang', (req, res) => {
const lang = req.params.lang || 'en'; // Default to English
let languageData;
// Try to load the correct language file, fall back to English
try {
languageData = getLanguageFile(lang);
} catch (err) {
languageData = getLanguageFile('en');
}
res.render('card', { langData: languageData});
});
module.exports = router;
+27
View File
@@ -0,0 +1,27 @@
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs');
// Function to load the appropriate language file
function getLanguageFile(language) {
const filePath = path.join(__dirname, `../locales/countdown/${language}.json`);
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
// Route for the card, with language support
router.get('/:lang', (req, res) => {
const lang = req.params.lang || 'en'; // Default to English
let languageData;
// Try to load the correct language file, fall back to English
try {
languageData = getLanguageFile(lang);
} catch (err) {
languageData = getLanguageFile('en');
}
res.render('countdown', { langData: languageData});
});
module.exports = router;
+71
View File
@@ -0,0 +1,71 @@
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const crypto = require('crypto');
// Configure multer storage to create unique filenames
const storage = multer.diskStorage({
destination: 'public/upload',
filename: (req, file, cb) => {
const originalName = path.parse(file.originalname);
const uniqueSuffix = Date.now() + '-' + crypto.randomBytes(4).toString('hex'); // Generate a unique suffix
const newFilename = `${originalName.name}-${uniqueSuffix}${originalName.ext}`; // Create new filename
cb(null, newFilename); // Save the file with the new name
}
});
const upload = multer({ storage }); // Use the custom storage configuration
const messagesFilePath = path.join(__dirname, '../messages.json');
// Function to load the appropriate language file
function getLanguageFile(language) {
const filePath = path.join(__dirname, `../locales/guestbook/${language}.json`);
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
// Load previous messages from a JSON file
let messages = [];
if (fs.existsSync(messagesFilePath)) {
messages = JSON.parse(fs.readFileSync(messagesFilePath, 'utf8'));
}
// Route for the guestbook, with language support
router.get('/:lang', (req, res) => {
const lang = req.params.lang || 'en'; // Default to English
let languageData;
// Try to load the correct language file, fall back to English
try {
languageData = getLanguageFile(lang);
} catch (err) {
languageData = getLanguageFile('en');
}
// Render the guestbook with messages
res.render('guestbook', { langData: languageData, messages });
});
// Endpoint to handle guestbook form submission
router.post('/new_comment', upload.single('file'), (req, res) => {
const lang = req.params.lang || 'en'; // Get the language from the URL
const comment = req.body.comment;
const name = req.body.name;
const imagePath = req.file ? path.join('/upload', req.file.filename) : null;
const originalFilename = req.file ? req.file.originalname : null; // Get the original filename
// Add the new message to the messages array
messages.push({ name, comment, imagePath, originalFilename });
// Save messages to a JSON file
try {
fs.writeFileSync(messagesFilePath, JSON.stringify(messages, null, 2)); // Indent for readability
} catch (error) {
console.error('Error writing to messages.json:', error);
}
res.redirect(`/guestbook/${lang}`); // Redirect to the guestbook page with language
});
module.exports = router;
+215
View File
@@ -0,0 +1,215 @@
<!DOCTYPE html>
<html lang="<%= langData.lang %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1024">
<meta property="og:title" content="<%= langData.title %>" />
<meta property="og:description" content="<%= langData.description %>" />
<meta property="og:image" content="/previews/card.png" />
<meta property="og:url" content="/card/<%= langData.lang %>" />
<meta property="og:type" content="website" />
<title>
<%= langData.title %>
</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link href="https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/card.css">
</head>
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
<body>
<!-- Language Switcher -->
<div class="language-switcher">
<a href="/card/en">English</a> |
<a href="/card/de">Deutsch</a>
</div>
<!-- Menu Button and Menu -->
<header>
<button class="menu-btn" id="menu-btn" onclick="toggleMenu()">Menu</button>
<nav id="menu" class="retractable-menu">
<ul>
<li><a href="/card/<%= langData.lang %>">
<%= langData.menu_card %>
</a></li>
<li><a href="/guestbook/<%= langData.lang %>">
<%= langData.menu_guestbook %>
</a></li>
<li><a href="/countdown/<%= langData.lang %>">
<%= langData.menu_countdown %>
</a></li>
<!-- Add more menu items as needed -->
</ul>
</nav>
</header>
<script>
function toggleMenu() {
const menu = document.getElementById('menu');
menu.classList.toggle('open');
}
</script>
<div class="hover-hint"> <%= langData.hover_hint %> </div>
<div class="invitation-card">
<div class="card-inner">
<!-- Front of the card -->
<div class="card-front">
<img src="/images/card_front.png" alt="Front of Invitation" class="card-image">
<div class="title-text">
<h1>
<u style="text-decoration-color: rgba(249, 104, 14, 0.671); text-decoration-thickness: 2px">Save the Date</u>
</h1>
</div>
</div>
<!-- Back of the card -->
<div class="card-back">
<img src="/images/card_back.png" alt="Back of Invitation" class="card-image">
<div class="card-text custom-text-align">
<p>
<%= langData.front_invation_1 %>
</p>
<p>
<%= langData.front_invation_2 %>
</p>
<p>
<%= langData.front_invation_3 %>
</p>
<br>
<h2>
<%= langData.back_info %>
</h2>
<br>
<p>
<%= langData.back_style %>
</p>
<p>
<%= langData.back_rules %>
</p>
<br>
<p>
<%= langData.back_afterword %>
</p>
<p>
<%= langData.back_signed %>
</p>
<br><br>
</div>
</div>
</div>
</div>
<script>
// <h2>
// <p>
// <%= langData.back_countdown %>
// <div id="countdown"></div>
// </p>
// </h2>
// // Convert the Unix timestamp (seconds) to milliseconds
// const targetDate = new Date(1727528400 * 1000);
// function updateCountdown() {
// const now = new Date();
// const timeDifference = targetDate - now;
// if (timeDifference <= 0) {
// document.getElementById('countdown').innerHTML = 'Time\'s up!';
// clearInterval(countdownInterval); // Stop the interval when the countdown reaches 0
// return;
// }
// const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
// const hours = Math.floor((timeDifference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
// const minutes = Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60));
// const seconds = Math.floor((timeDifference % (1000 * 60)) / 1000);
// document.getElementById('countdown').innerHTML =
// days + "d " + hours + "h " + minutes + "m " + seconds + "s ";
// }
// // Update the countdown every second
// const countdownInterval = setInterval(updateCountdown, 1000);
// updateCountdown(); // Initial call to set the countdown immediately
document.querySelector('.invitation-card').addEventListener('mouseenter', () => {
var duration = 1 * 1000;
var animationEnd = Date.now() + duration;
var skew = 1;
function randomInRange(min, max) {
return Math.random() * (max - min) + min;
}
(function frame() {
var timeLeft = animationEnd - Date.now();
var ticks = Math.max(200, 500 * (timeLeft / duration));
skew = Math.max(0.8, skew - 0.001);
confetti({
particleCount: 1,
startVelocity: 0,
ticks: ticks,
origin: {
x: Math.random(),
// since particles fall down, skew start toward the top
y: (Math.random() * skew) - 0.2
},
colors: ['#da7ff0'],
shapes: ['circle'],
gravity: randomInRange(0.4, 0.6),
scalar: randomInRange(0.4, 2),
drift: randomInRange(-0.4, 1)
});
if (timeLeft > 0) {
requestAnimationFrame(frame);
}
}());
});
document.addEventListener('DOMContentLoaded', function() {
const card = document.querySelector('.invitation-card');
const hoverHint = document.querySelector('.hover-hint');
let hoverTimeout;
function showHint() {
hoverHint.style.opacity = '1';
}
function hideHint() {
hoverHint.style.opacity = '0';
}
// Show the hover hint after 5 seconds
hoverTimeout = setTimeout(showHint, 5000);
// On hover, flip the card and hide the hover hint
card.addEventListener('mouseenter', function() {
hideHint();
clearTimeout(hoverTimeout);
card.classList.add('flipped');
});
// On mouse leave, flip the card back and reset hint timer
card.addEventListener('mouseleave', function() {
card.classList.remove('flipped');
hoverTimeout = setTimeout(showHint, 5000);
});
});
</script>
</body>
</html>
+93
View File
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="<%= langData.lang %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1024">
<meta property="og:title" content="<%= langData.title %>" />
<meta property="og:description" content="<%= langData.description %>" />
<meta property="og:image" content="/previews/countdown.png" />
<meta property="og:url" content="/countdown/<%= langData.lang %>" />
<meta property="og:type" content="website" />
<title>
<%= langData.title %>
</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link href="https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/countdown.css">
</head>
<body>
<!-- Language Switcher -->
<div class="language-switcher">
<a href="/countdown/en">English</a> |
<a href="/countdown/de">Deutsch</a>
</div>
<!-- Menu Button and Menu -->
<header>
<button class="menu-button" id="menu-button" onclick="toggleMenu()">Menu</button>
<nav id="menu" class="retractable-menu">
<ul>
<li><a href="/card/<%= langData.lang %>">
<%= langData.menu_card %>
</a></li>
<li><a href="/guestbook/<%= langData.lang %>">
<%= langData.menu_guestbook %>
</a></li>
<li><a href="/countdown/<%= langData.lang %>">
<%= langData.menu_countdown %>
</a></li>
<!-- Add more menu items as needed -->
</ul>
</nav>
</header>
<script>
function toggleMenu() {
const menu = document.getElementById('menu');
menu.classList.toggle('open');
}
</script>
<div class="countdown-card">
<p class="countdown-title"></p>
<h2 class="countdown">
<u style="color: rgba(0, 0, 0, 0.5); text-decoration-color: rgba(0, 0, 0, 0.5); text-decoration-thickness: 2px"><%= langData.countdown_text %></u>
<p>
<div id="countdown"></div>
</p>
</h2>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
//Convert the Unix timestamp (seconds) to milliseconds
const targetDate = new Date(1727528400 * 1000);
const finishMessage = "<%= langData.countdown_finished %>";
function updateCountdown() {
const now = new Date();
const timeDifference = targetDate - now;
if (timeDifference <= 0) {
document.getElementById('countdown').innerHTML = finishMessage;
clearInterval(countdownInterval); //Stop the interval when the countdown reaches 0
return;
}
const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
const hours = Math.floor((timeDifference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeDifference % (1000 * 60)) / 1000);
document.getElementById('countdown').innerHTML =
days + "d " + hours + "h " + minutes + "m " + seconds + "s ";
}
//Update the countdown every second
const countdownInterval = setInterval(updateCountdown, 1000);
updateCountdown(); //Initial call to set the countdown immediately
</script>
</html>
+145
View File
@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html lang="<%= langData.lang %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
<%= langData.title %>
</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link href="https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/guestbook.css">
</head>
<body>
<!-- Language Switcher -->
<div class="language-switcher">
<a href="/guestbook/en">English</a> |
<a href="/guestbook/de">Deutsch</a>
</div>
<!-- Menu Button and Menu -->
<header>
<button class="menu-button" id="menu-button" onclick="toggleMenu()">Menu</button>
<nav id="menu" class="retractable-menu">
<ul>
<li><a href="/card/<%= langData.lang %>">
<%= langData.menu_card %>
</a></li>
<li><a href="/guestbook/<%= langData.lang %>">
<%= langData.menu_guestbook %>
</a></li>
<li><a href="/countdown/<%= langData.lang %>">
<%= langData.menu_countdown %>
</a></li>
<!-- Add more menu items as needed -->
</ul>
</nav>
</header>
<script>
function toggleMenu() {
const menu = document.getElementById('menu');
menu.classList.toggle('open');
}
</script>
<div class="container mt-5">
<div class="text-center mb-4">
<h1 class="display-4 text-white">
<%= langData.guestbook_welcome %>
</h1>
</div>
<div class="card mb-4">
<div class="card-body">
<form action="/guestbook/new_comment" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="name">
<%= langData.guestbook_name %>
</label>
<input type="text" name="name" id="name" class="form-control" required
placeholder="<%= langData.guestbook_name_placeholder %>">
</div>
<div class="form-group">
<label for="comment">
<%= langData.guestbook_comment %>
</label>
<textarea name="comment" id="comment" class="form-control" required
placeholder="<%= langData.guestbook_comment_placeholder %>"></textarea>
</div>
<div class="form-group">
<label for="file">
<%= langData.guestbook_upload %>
</label>
<input type="file" name="file" id="file" class="form-control p-1">
</div>
<button type="submit" class="btn btn-primary">
<%= langData.guestbook_submit %>
</button>
</form>
</div>
</div>
<!-- Scrollable Previous Messages Section -->
<div class="previous-messages">
<h2>
<%= langData.previous_messages %>
</h2>
<!-- Modal Trigger in your previous-messages section -->
<div class="list-group scrollable-messages">
<% messages.forEach(message=> { %>
<div class="list-group-item">
<h5 class="mb-1">
<%= message.name %>
<%= langData.messages_wrote %>
</h5>
<p class="mb-1">
<%= message.comment %>
</p>
<% if (message.imagePath) { %>
<!-- Modal Trigger: Click on the image to open the modal -->
<img src="<%= message.imagePath %>" class="img-fluid uploaded-image"
alt="User Uploaded Image" data-toggle="modal" data-target="#imageModal"
data-image="<%= message.imagePath %>">
<% } %>
</div>
<% }); %>
</div>
<!-- Modal Structure -->
<div class="modal fade" id="imageModal" tabindex="-1" aria-labelledby="imageModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imageModalLabel">Image Preview</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<img src="" id="modalImage" class="img-fluid" alt="Selected Image">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- jQuery and Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
$('#imageModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget); // Button that triggered the modal
var imageUrl = button.data('image'); // Extract image URL from data-image attribute
var modal = $(this);
modal.find('.modal-body #modalImage').attr('src', imageUrl); // Update the image in the modal
});
</script>
</body>
</html>