├── .gitignore
├── public
└── favicon.ico
├── src
├── assets
│ ├── images
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ ├── 5.png
│ │ ├── bca.png
│ │ ├── bri.png
│ │ ├── cewe.png
│ │ ├── cowo.png
│ │ ├── couple.png
│ │ ├── readme1.png
│ │ ├── readme2.png
│ │ ├── readme3.png
│ │ ├── readme4.png
│ │ ├── readme5.png
│ │ ├── readme6.png
│ │ ├── readme7.png
│ │ └── readme8.png
│ ├── audio
│ │ └── wedding.mp3
│ ├── styles
│ │ └── global.css
│ └── data
│ │ └── data.js
├── main.js
├── js
│ ├── bride.js
│ ├── time.js
│ ├── navbar.js
│ ├── galeri.js
│ ├── home.js
│ ├── welcome.js
│ └── wishas.js
├── css
│ ├── footer.css
│ ├── navbar.css
│ ├── time.css
│ ├── welcome.css
│ ├── home.css
│ ├── bride.css
│ ├── mediaQueries.css
│ ├── galeri.css
│ └── wishas.css
├── services
│ └── comentarService.js
└── utils
│ └── helper.js
├── README.md
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/1.png
--------------------------------------------------------------------------------
/src/assets/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/2.png
--------------------------------------------------------------------------------
/src/assets/images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/3.png
--------------------------------------------------------------------------------
/src/assets/images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/4.png
--------------------------------------------------------------------------------
/src/assets/images/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/5.png
--------------------------------------------------------------------------------
/src/assets/images/bca.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/bca.png
--------------------------------------------------------------------------------
/src/assets/images/bri.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/bri.png
--------------------------------------------------------------------------------
/src/assets/images/cewe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/cewe.png
--------------------------------------------------------------------------------
/src/assets/images/cowo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/cowo.png
--------------------------------------------------------------------------------
/src/assets/audio/wedding.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/audio/wedding.mp3
--------------------------------------------------------------------------------
/src/assets/images/couple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/couple.png
--------------------------------------------------------------------------------
/src/assets/images/readme1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/readme1.png
--------------------------------------------------------------------------------
/src/assets/images/readme2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/readme2.png
--------------------------------------------------------------------------------
/src/assets/images/readme3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/readme3.png
--------------------------------------------------------------------------------
/src/assets/images/readme4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/readme4.png
--------------------------------------------------------------------------------
/src/assets/images/readme5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/readme5.png
--------------------------------------------------------------------------------
/src/assets/images/readme6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/readme6.png
--------------------------------------------------------------------------------
/src/assets/images/readme7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/readme7.png
--------------------------------------------------------------------------------
/src/assets/images/readme8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdprdh/wedding-invitation/HEAD/src/assets/images/readme8.png
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import {home} from "./js/home.js";
2 | import {bride} from "./js/bride.js";
3 | import {time} from "./js/time.js";
4 | import {galeri} from "./js/galeri.js";
5 | import {wishas} from "./js/wishas.js";
6 | import {navbar} from "./js/navbar.js";
7 | import {welcome} from "./js/welcome.js";
8 |
9 | // load content
10 | document.addEventListener('DOMContentLoaded', () => {
11 | AOS.init();
12 |
13 | welcome();
14 | navbar();
15 | home();
16 | bride()
17 | time();
18 | galeri();
19 | wishas();
20 | });
--------------------------------------------------------------------------------
/src/js/bride.js:
--------------------------------------------------------------------------------
1 | import {data} from "../assets/data/data.js";
2 | import {renderElement} from "../utils/helper.js";
3 |
4 | export const bride = () => {
5 | const brideCouple = document.querySelector('.bride_couple ul');
6 | const brideListItem = (data) => (
7 | `
8 |
9 |
10 | ${data.name}
11 |
12 | ${data.child} dari Bapak ${data.father} & Ibu ${data.mother}
13 | &
14 | `
15 | )
16 |
17 | const brideData = [data.bride.L, data.bride.P];
18 |
19 | renderElement(brideData, brideCouple, brideListItem);
20 | }
--------------------------------------------------------------------------------
/src/js/time.js:
--------------------------------------------------------------------------------
1 | import { data } from "../assets/data/data.js";
2 |
3 | export const time = () => {
4 | const timeContainer = document.querySelector('.time');
5 | const [marriageDiv, receptionDiv] = timeContainer.querySelectorAll('div div');
6 | const mapLink = timeContainer.querySelector('a');
7 | const addressParagraph = timeContainer.querySelector('a + p');
8 |
9 | const createTimeListItem = (title, details) => (
10 | `${title}
11 | ${details.day}, ${details.date} ${details.month} ${details.year}
12 | Pukul ${details.hours.start} WIB sd ${details.hours.finish}
`
13 | );
14 |
15 | marriageDiv.innerHTML = createTimeListItem('Akad', data.time.marriage);
16 | receptionDiv.innerHTML = createTimeListItem('Resepsi', data.time.reception);
17 |
18 | mapLink.href = data.link.map;
19 | addressParagraph.textContent = data.time.address;
20 | };
21 |
--------------------------------------------------------------------------------
/src/css/footer.css:
--------------------------------------------------------------------------------
1 | footer {
2 | margin-top: 6rem;
3 | width: 100%;
4 | padding: 4rem 8rem;
5 | text-align: center;
6 | }
7 |
8 | footer h2 {
9 | font-size: var(--fontSize-heading-base);
10 | font-family: var(--sacramento);
11 | font-weight: 400;
12 | margin-top: 1rem;
13 | }
14 |
15 | footer h4 {
16 | font-size: 1.6rem;
17 | font-family: var(--arabic);
18 | font-weight: 400;
19 | }
20 |
21 | footer > div {
22 | width: 100%;
23 | display: flex;
24 | align-items: center;
25 | justify-content: space-between;
26 | border-top: 1px solid #000;
27 | margin-top: 1rem;
28 | padding-top: 0.6rem;
29 | }
30 |
31 | footer > div p {
32 | font-size: 0.6rem;
33 | }
34 |
35 | footer > div p span {
36 | font-weight: bold;
37 | }
38 |
39 | footer > div a {
40 | display: flex;
41 | align-items: center;
42 | justify-content: center;
43 | color: #000;
44 | font-size: 0.8rem;
45 | }
--------------------------------------------------------------------------------
/src/js/navbar.js:
--------------------------------------------------------------------------------
1 | import {data} from "../assets/data/data.js";
2 | import {renderElement} from "../utils/helper.js";
3 |
4 | export const navbar = () => {
5 | const containerNavbar = document.querySelector('nav');
6 | const audioButton = document.querySelector('.audio button');
7 |
8 | const listItemNavbar = (data) => (
9 | `
10 |
11 | ${data.teks}
12 | `
13 | );
14 |
15 | renderElement(data.navbar, containerNavbar, listItemNavbar);
16 |
17 | let lastScrollY = window.scrollY;
18 |
19 | document.addEventListener('scroll', () => {
20 | const currentScrollY = window.scrollY;
21 | if (currentScrollY < lastScrollY) {
22 | containerNavbar.classList.remove('scroll');
23 | } else {
24 | containerNavbar.classList.add('scroll');
25 | }
26 | lastScrollY = currentScrollY;
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/css/navbar.css:
--------------------------------------------------------------------------------
1 | header {
2 | width: 100%;
3 | }
4 |
5 | header nav {
6 | display: none;
7 | width: 100%;
8 | padding: 0 1.4rem;
9 | background-color: var(--bg-color);
10 | align-items: center;
11 | justify-content: space-between;
12 | position: fixed;
13 | left: 0;
14 | right: 0;
15 | bottom: -100%;
16 | z-index: 99;
17 | transform: translateY(0);
18 | transition: bottom 2s ease, transform 1s ease;
19 | }
20 |
21 | header nav.active {
22 | bottom: 0;
23 | }
24 |
25 | header nav.scroll {
26 | transform: translateY(100%);
27 | }
28 |
29 | header nav a {
30 | display: flex;
31 | align-items: center;
32 | justify-content: center;
33 | flex-direction: column;
34 | gap: 0.1rem;
35 | width: 4rem;
36 | height: 3rem;
37 | color: #000;
38 | }
39 |
40 | header nav a i {
41 | font-size: 1.2rem;
42 | }
43 |
44 | header nav a span {
45 | font-size: 0.6rem;
46 | }
47 |
48 | header nav a:hover {
49 | background-color: #000;
50 | color: #fff;
51 | }
--------------------------------------------------------------------------------
/src/css/time.css:
--------------------------------------------------------------------------------
1 | .time {
2 | width: 100%;
3 | min-height: 100vh;
4 | text-align: center;
5 | padding: 10rem 8rem;
6 | position: relative;
7 | }
8 |
9 | .time h2 {
10 | font-family: var(--sacramento);
11 | font-size: var(--fontSize-heading-base);
12 | font-weight: 400;
13 | }
14 |
15 | .time div:nth-of-type(1) {
16 | margin-top: 2rem;
17 | }
18 |
19 | .time div:nth-of-type(1) div h3 {
20 | font-family: var(--sacramento);
21 | font-size: var(--fontSize-heading-lg);
22 | font-weight: 400;
23 | }
24 |
25 | .time div:nth-of-type(1) div:nth-of-type(2){
26 | margin-top: 2rem;
27 | }
28 |
29 | .time a {
30 | padding: 0.5rem 1rem;
31 | color: #fff;
32 | font-size: 1rem;
33 | background-color: #000;
34 | margin: 2rem auto 1rem;
35 | border-radius: 20px;
36 | width: fit-content;
37 | display: flex;
38 | align-items: center;
39 | justify-content: center;
40 | gap: 0.5rem;
41 | }
42 |
43 | .time a i {
44 | font-size: 1.2rem;
45 | }
46 |
47 | .time a + p + i {
48 | right: 8rem;
49 | top: 22rem;
50 | }
--------------------------------------------------------------------------------
/src/services/comentarService.js:
--------------------------------------------------------------------------------
1 | import {data} from "../assets/data/data.js";
2 |
3 | export const comentarService = {
4 | getComentar: async function () {
5 | try {
6 | const response = await fetch(data.api);
7 | return await response.json();
8 | } catch (error) {
9 | return {error: error && error.message};
10 | }
11 | },
12 |
13 | addComentar: async function ({id, name, status, message, date, color}) {
14 | const comentar = {
15 | id: id,
16 | name: name,
17 | status: status,
18 | message: message,
19 | date: date,
20 | color: color,
21 | };
22 |
23 | try {
24 | const response = await fetch(data.api, {
25 | method: 'POST',
26 | mode: 'no-cors',
27 | headers: {
28 | 'Content-Type': 'application/json'
29 | },
30 | body: JSON.stringify(comentar),
31 | });
32 |
33 | return await response.json();
34 |
35 | } catch (error) {
36 | console.error('Post error:', error);
37 | return {error: error.message};
38 | }
39 | },
40 | };
--------------------------------------------------------------------------------
/src/css/welcome.css:
--------------------------------------------------------------------------------
1 | .welcome {
2 | width: 100%;
3 | height: 100vh;
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | flex-direction: column;
8 | text-align: center;
9 | background-color: #212529;
10 | position: fixed;
11 | top: 0;
12 | left: 0;
13 | right: 0;
14 | bottom: 0;
15 | z-index: 99;
16 | color: #fff;
17 | transition: all 2s ease-in;
18 | opacity: 0;
19 | transform: translateY(-100%);
20 | }
21 |
22 | .welcome.active {
23 | transform: translateY(0);
24 | opacity: 1;
25 | }
26 |
27 | .welcome.hide {
28 | top: -100%;
29 | opacity: 0;
30 | }
31 |
32 | .welcome h2 {
33 | font-size: 2.4rem;
34 | font-family: var(--sacramento);
35 | font-weight: 400;
36 | }
37 |
38 | .welcome figure {
39 | display: inherit;
40 | align-items: inherit;
41 | justify-content: inherit;
42 | flex-direction: inherit;
43 | margin: 1rem 0;
44 | }
45 |
46 | .welcome figure img {
47 | width: 10rem;
48 | height: 10rem;
49 | border-radius: 50%;
50 | object-fit: cover;
51 | box-shadow: var(--shadow-md);
52 | border: 2px solid #fff;
53 | }
54 |
55 | .welcome figure figcaption {
56 | font-size: var(--fontSize-heading-base);
57 | font-family: var(--sacramento);
58 | font-weight: 400;
59 | margin-top: 0.5rem;
60 | }
61 |
62 | .welcome p span {
63 | font-size: 2rem;
64 | margin: 1rem 0;
65 | display: block;
66 | }
67 |
68 | .welcome button {
69 | display: inherit;
70 | align-items: inherit;
71 | justify-content: inherit;
72 | gap: 0.4rem;
73 | color: #000;
74 | padding: 0.5rem 1rem;
75 | border: none;
76 | transition: var(--transition-small);
77 | border-radius: 15px;
78 | cursor: pointer;
79 | }
80 |
81 | .welcome button:hover {
82 | color: #fff;
83 | background-color: #000;
84 | }
85 |
86 | .welcome button i {
87 | font-size: 1.2rem;
88 | }
--------------------------------------------------------------------------------
/src/assets/styles/global.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary-font-color: '#212529';
3 | --bg-color: #f8f9fa;
4 |
5 | --fontSize-heading-lg: 3rem;
6 | --fontSize-heading-base: 2.8rem;
7 | --fontSize-text-base: 1rem;
8 |
9 | --sacramento: Sacramento, cursive;
10 | --arabic: 'Noto Naskh Arabic', serif;
11 |
12 | --shadow-md: 0 0 10px rgba(0, 0, 0, 0.3);
13 |
14 | --transition-small: all .3s ease;
15 | }
16 |
17 | html {
18 | scroll-behavior: smooth;
19 | scrollbar-width: none !important;
20 | }
21 |
22 | * {
23 | margin: 0;
24 | padding: 0;
25 | box-sizing: border-box;
26 | text-decoration: none;
27 | outline: none;
28 | }
29 |
30 | body {
31 | font-family: 'Josefin Sans', sans-serif;
32 | color: var(--primary-font-color);
33 | overflow-y: hidden;
34 | }
35 |
36 | body.active {
37 | overflow-y: auto;
38 | }
39 |
40 | body::-webkit-scrollbar {
41 | display: none;
42 | }
43 |
44 | p {
45 | line-height: 1.7rem;
46 | }
47 |
48 | .audio button {
49 | width: 2rem;
50 | height: 2rem;
51 | border-radius: 50%;
52 | border: none;
53 | background-color: #fff;
54 | color: #000;
55 | font-size: 1.2rem;
56 | display: flex;
57 | align-items: center;
58 | justify-content: center;
59 | position: fixed;
60 | right: 1rem;
61 | bottom: 1rem;
62 | cursor: pointer;
63 | transform: translateX(200%);
64 | transition: 1.5s transform ease;
65 | }
66 |
67 | .audio button.show {
68 | transform: translateX(0);
69 | }
70 |
71 | .audio button.active {
72 | animation: rotate 3s linear infinite;
73 | }
74 |
75 |
76 | @keyframes rotate {
77 | 0% {
78 | transform: rotate(0deg);
79 | }
80 |
81 | 100% {
82 | transform: rotate(360deg);
83 | }
84 | }
85 |
86 | @keyframes upAndDown {
87 | 0%, 100% {
88 | transform: translateY(-0.8rem);
89 | }
90 | 50% {
91 | transform: translateY(0.8rem);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/css/home.css:
--------------------------------------------------------------------------------
1 | .home {
2 | width: 100%;
3 | height: 100vh;
4 | text-align: center;
5 | background-color: var(--bg-color);
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | flex-direction: column;
10 | position: relative;
11 | transition: all 2s ease;
12 | transform: translateY(-100%);
13 | }
14 |
15 | .home.active {
16 | transform: translateY(0);
17 | }
18 |
19 | .home h2 {
20 | font-size: 2.4rem;
21 | font-family: var(--sacramento);
22 | font-weight: 400;
23 | }
24 |
25 | .home figure {
26 | margin: 2rem auto 0;
27 | }
28 |
29 | .home figure img {
30 | width: 10rem;
31 | height: 10rem;
32 | border-radius: 50%;
33 | object-fit: cover;
34 | box-shadow: var(--shadow-md);
35 | }
36 |
37 | .home figure figcaption {
38 | margin: 0 0 0.5rem;
39 | font-weight: 400;
40 | font-family: var(--sacramento);
41 | font-size: var(--fontSize-heading-lg);
42 | }
43 |
44 | .home h3 {
45 | font-weight: 500;
46 | }
47 |
48 | .home .home-time {
49 | margin-top: 1rem;
50 | display: flex;
51 | align-items: center;
52 | justify-content: center;
53 | gap: 1rem;
54 | }
55 |
56 | .home .home-time div {
57 | width: 3rem;
58 | height: 3rem;
59 | display: flex;
60 | align-items: center;
61 | justify-content: center;
62 | border: 1px solid #000;
63 | border-radius: 8px;
64 | }
65 |
66 | .home .home-time div p {
67 | font-size: 0.8rem;
68 | line-height: normal;
69 | }
70 |
71 | .home .home-time div p span {
72 | font-size: 0.6rem;
73 | margin-top: 0.2rem;
74 | }
75 |
76 | .home a {
77 | padding: 0.5rem 1rem;
78 | color: #fff;
79 | font-size: 1rem;
80 | background-color: #000;
81 | margin: 1rem auto;
82 | border-radius: 20px;
83 | width: fit-content;
84 | display: flex;
85 | align-items: center;
86 | justify-content: center;
87 | gap: 0.5rem;
88 | }
89 |
90 | .home a i {
91 | font-size: 1.2rem;
92 | }
93 |
94 | .home .scroll_down {
95 | display: flex;
96 | align-items: center;
97 | justify-content: center;
98 | flex-direction: column;
99 | gap: 1rem;
100 | }
101 |
102 | .home svg {
103 | position: absolute;
104 | left: 0;
105 | right: 0;
106 | bottom: -14rem;
107 | z-index: -10;
108 | }
--------------------------------------------------------------------------------
/src/css/bride.css:
--------------------------------------------------------------------------------
1 | .bride {
2 | width: 100%;
3 | min-height: 100vh;
4 | text-align: center;
5 | padding: 10rem 8rem;
6 | position: relative;
7 | }
8 |
9 | .bride h2 {
10 | font-size: 1.6rem;
11 | font-family: var(--arabic);
12 | font-weight: 400;
13 | }
14 |
15 | .bride h3 {
16 | font-size: var(--fontSize-heading-base);
17 | font-family: var(--sacramento);
18 | font-weight: 400;
19 | margin: 1rem 0;
20 | }
21 |
22 | .bride .bride_couple,
23 | .bride .bride_couple ul,
24 | .bride .bride_couple li,
25 | .bride .bride_couple li figure {
26 | width: 100%;
27 | display: flex;
28 | align-items: center;
29 | justify-content: center;
30 | flex-direction: column;
31 | }
32 |
33 | .bride .bride_couple {
34 | margin-top: 4rem;
35 | }
36 |
37 | .bride .bride_couple ul {
38 | gap: 1rem;
39 | }
40 |
41 | .bride .bride_couple li figure img {
42 | width: 12rem;
43 | height: 12rem;
44 | object-fit: cover;
45 | border-radius: 50%;
46 | box-shadow: var(--shadow-md);
47 | }
48 |
49 | .bride .bride_couple li figure figcaption {
50 | font-size: var(--fontSize-heading-base);
51 | margin: 1rem 0;
52 | font-family: var(--sacramento);
53 | }
54 |
55 | .bride .bride_couple span {
56 | font-size: 3.4rem;
57 | font-family: var(--sacramento);
58 | font-weight: 400;
59 | margin-top: 1rem;
60 | }
61 |
62 | .bride i, .hadist i, .time a + p + i, .galeri > i {
63 | font-size: 3rem;
64 | color: #908d8d;
65 | position: absolute;
66 | z-index: 10;
67 | animation: upAndDown 3s infinite;
68 | }
69 |
70 | .bride i:nth-of-type(1) {
71 | top: 14rem;
72 | right: 8rem;
73 | }
74 |
75 | .bride i:nth-of-type(2){
76 | left: 8rem;
77 | top: 24rem;
78 | }
79 |
80 | .bride i:nth-of-type(3) {
81 | top: 54rem;
82 | right: 8rem;
83 | }
84 |
85 | .bride i:nth-of-type(4){
86 | top: 60rem;
87 | left: 8rem;
88 | }
89 |
90 | .bride i:nth-of-type(5){
91 | right: 8rem;
92 | bottom: 6rem;
93 | }
94 |
95 | .hadist {
96 | width: 100%;
97 | padding: 4rem 8rem;
98 | text-align: center;
99 | background-color: var(--bg-color);
100 | position: relative;
101 | }
102 |
103 | .hadist h2 {
104 | font-size: var(--fontSize-heading-base);
105 | font-family: var(--sacramento);
106 | font-weight: 400;
107 | margin-bottom: 1rem;
108 | }
109 |
110 | .hadist p span {
111 | display: block;
112 | margin-top: 1rem;
113 | font-size: 1.1rem;
114 | }
115 |
116 | .hadist i {
117 | top: 16rem;
118 | left: 8rem;
119 | }
120 |
121 | .hadist svg {
122 | position: absolute;
123 | left: 0;
124 | right: 0;
125 | z-index: -10;
126 | }
127 |
128 | .hadist svg:nth-of-type(1){
129 | top: -8rem;
130 | }
131 |
132 | .hadist svg:nth-of-type(2){
133 | bottom: -15rem;
134 | }
135 |
--------------------------------------------------------------------------------
/src/utils/helper.js:
--------------------------------------------------------------------------------
1 | export const generateRandomId = () => {
2 | return Math.floor(100000 + Math.random() * 900000);
3 | }
4 |
5 | export const generateRandomColor = () => {
6 | const avoidColors = ['#000000', '#ffffff', '#f0f0f0', '#f5f5f5', '#fafafa'];
7 |
8 | let color = `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`;
9 |
10 | while (avoidColors.includes(color)) {
11 | color = `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`;
12 | }
13 |
14 | return color;
15 | }
16 |
17 | export const renderElement = (data, element, listItem) => {
18 | element.innerHTML = '';
19 | data.map((data) => element.innerHTML += listItem(data));
20 | }
21 |
22 | export const addClassElement = (element, newClass) => {
23 | element.classList.add(newClass);
24 | }
25 |
26 | export const removeClassElement = (element, currentClass) => {
27 | element.classList.remove(currentClass);
28 | }
29 |
30 | export const formattedDate = (date) => {
31 | const d1 = new Date(date);
32 | const d2 = new Date();
33 |
34 | const diffInMs = Math.abs(d2 - d1);
35 |
36 | const msInMinute = 60 * 1000;
37 | const msInHour = 60 * msInMinute;
38 | const msInDay = 24 * msInHour;
39 |
40 | const days = Math.floor(diffInMs / msInDay);
41 | const hours = Math.floor((diffInMs % msInDay) / msInHour);
42 | const minutes = Math.floor((diffInMs % msInHour) / msInMinute);
43 |
44 | return {
45 | days: days,
46 | hours: hours,
47 | minutes: minutes
48 | };
49 | }
50 |
51 | export const formattedName = (name) => {
52 | return name.toLowerCase()
53 | .split(' ')
54 | .map(word => word.charAt(0).toUpperCase() + word.slice(1))
55 | .join(' ');
56 | }
57 |
58 | export const getCurrentDateTime = () => {
59 | const now = new Date();
60 | let year = now.getFullYear();
61 | let month = (now.getMonth() + 1).toString().padStart(2, '0');
62 | let day = now.getDate().toString().padStart(2, '0');
63 | let hours = now.getHours().toString().padStart(2, '0');
64 | let minutes = now.getMinutes().toString().padStart(2, '0');
65 | return `${year}-${month}-${day} ${hours}:${minutes}`;
66 | };
67 |
68 | const monthMapping = {
69 | 'Januari': 1,
70 | 'Februari': 2,
71 | 'Maret': 3,
72 | 'April': 4,
73 | 'Mei': 5,
74 | 'Juni': 6,
75 | 'Juli': 7,
76 | 'Agustus': 8,
77 | 'September': 9,
78 | 'Oktober': 10,
79 | 'November': 11,
80 | 'Desember': 12
81 | };
82 |
83 | export const monthNameToNumber = (monthName) => {
84 | return monthMapping[monthName] || 'di awali huruf besar!';
85 | }
86 |
87 | export const getQueryParameter = (parameterName) => {
88 | const url = window.location.href;
89 | const queryString = url.split('?')[1]?.split('#')[0];
90 | if (!queryString) {
91 | return null;
92 | }
93 | const urlParams = new URLSearchParams(queryString);
94 | return urlParams.get(parameterName);
95 | }
--------------------------------------------------------------------------------
/src/js/galeri.js:
--------------------------------------------------------------------------------
1 | import {data} from "../assets/data/data.js";
2 |
3 | export const galeri = () => {
4 | const galeriElement = document.querySelector('.galeri');
5 | const showAllContainer = galeriElement.querySelector('div:nth-of-type(2)');
6 |
7 | const [_, figureElement, paginationElement, prevButton, nextButton, showAllButton] = galeriElement.children[0].children;
8 | const [__, showAllBox, closeButton] = showAllContainer.children;
9 |
10 | const initializeGallery = () => {
11 | const initialImage = data.galeri[0];
12 | figureElement.innerHTML = ` `;
13 |
14 | data.galeri.forEach((item, index) => {
15 | paginationElement.innerHTML += ` `;
16 | });
17 |
18 | updateNavigationButtons(initialImage.id);
19 | };
20 |
21 | const updateGalleryImage = (id) => {
22 | const selectedImage = data.galeri.find(item => item.id === id);
23 |
24 | if (selectedImage) {
25 | figureElement.innerHTML = ` `;
26 |
27 | paginationElement.querySelectorAll('li').forEach((li) => {
28 | li.classList.toggle('active', parseInt(li.dataset.id) === id);
29 | });
30 | }
31 | };
32 |
33 | const updateNavigationButtons = (id) => {
34 | nextButton.dataset.id = `${id}`;
35 | prevButton.dataset.id = `${id}`;
36 | };
37 |
38 | const autoPlayGallery = () => {
39 | let id = parseInt(nextButton.dataset.id);
40 | id = (id < data.galeri.length) ? id + 1 : 1;
41 | updateNavigationButtons(id);
42 | updateGalleryImage(id);
43 | };
44 |
45 | nextButton.addEventListener('click', () => {
46 | let id = parseInt(nextButton.dataset.id);
47 | if (id < data.galeri.length) {
48 | id++;
49 | updateNavigationButtons(id);
50 | updateGalleryImage(id);
51 | }
52 | });
53 |
54 | prevButton.addEventListener('click', () => {
55 | let id = parseInt(prevButton.dataset.id);
56 | if (id > 1) {
57 | id--;
58 | updateNavigationButtons(id);
59 | updateGalleryImage(id);
60 | }
61 | });
62 |
63 | showAllButton.addEventListener('click', () => {
64 | showAllBox.innerHTML = data.galeri.map(item => ` `).join('');
65 | showAllContainer.classList.add('active');
66 | });
67 |
68 | closeButton.addEventListener('click', () => {
69 | showAllBox.innerHTML = '';
70 | showAllContainer.classList.remove('active');
71 | });
72 |
73 | initializeGallery();
74 | setInterval(autoPlayGallery, 3000);
75 |
76 | paginationElement.querySelectorAll('li').forEach((pagination) => {
77 | pagination.addEventListener('click', (e) => {
78 | const id = +e.target.dataset.id;
79 | updateGalleryImage(id);
80 | })
81 | })
82 | };
83 |
--------------------------------------------------------------------------------
/src/js/home.js:
--------------------------------------------------------------------------------
1 | import {data} from "../assets/data/data.js";
2 | import {monthNameToNumber} from "../utils/helper.js";
3 |
4 | export const home = () => {
5 | const homeContainer = document.querySelector('.home');
6 | const [_, figureElement, timeElement, homeTime, calendarAnchor] = homeContainer.children;
7 |
8 | const generateFigureContent = ({bride}) => {
9 | const {L: {name: brideLName}, P: {name: bridePName}, couple: coupleImage} = bride;
10 | return `
11 |
12 |
13 | ${brideLName.split(' ')[0]} & ${bridePName.split(' ')[0]}
14 | `;
15 | };
16 |
17 | const generateTimeContent = ({time}) => {
18 | const {year, month, date, day} = time.marriage;
19 | return `
20 |
21 | ${day}, ${date} ${month} ${year}
22 | `;
23 | };
24 |
25 | const generateCountdownMarkup = (days, hours, minutes, seconds) => {
26 | return `
29 |
32 |
33 |
${minutes}Menit
34 |
35 |
36 |
${seconds}Detik
37 |
`;
38 | };
39 |
40 | const updateCountdown = (endTime, homeTime) => {
41 | const now = new Date().getTime();
42 | const distance = endTime - now;
43 |
44 | const days = Math.floor(distance / (1000 * 60 * 60 * 24));
45 | const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
46 | const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
47 | const seconds = Math.floor((distance % (1000 * 60)) / 1000);
48 |
49 | if (distance < 0) {
50 | clearInterval(intervalId);
51 | homeTime.innerHTML = generateCountdownMarkup(0, 0, 0, 0);
52 | } else {
53 | homeTime.innerHTML = generateCountdownMarkup(days, hours, minutes, seconds);
54 | }
55 | };
56 |
57 | const startCountdown = (homeTime, timeData) => {
58 | const {year, month, date} = timeData.marriage;
59 | const endTime = new Date(`${String(year)}-${String(monthNameToNumber(month)).padStart(2, '0')}-${String(date).padStart(2, '0')}T00:00:00`);
60 |
61 | updateCountdown(endTime, homeTime);
62 | setInterval(() => updateCountdown(endTime, homeTime), 1000);
63 | };
64 |
65 | const initializeHome = () => {
66 | const {bride, time, link} = data;
67 | figureElement.innerHTML = generateFigureContent({bride});
68 | timeElement.innerHTML = generateTimeContent({time});
69 | calendarAnchor.href = link.calendar;
70 | startCountdown(homeTime, time);
71 | };
72 |
73 | initializeHome();
74 | };
75 |
--------------------------------------------------------------------------------
/src/js/welcome.js:
--------------------------------------------------------------------------------
1 | import {data} from "../assets/data/data.js";
2 | import {addClassElement, getQueryParameter, removeClassElement} from "../utils/helper.js";
3 |
4 | export const welcome = () => {
5 | const welcomeElement = document.querySelector('.welcome');
6 | const homeElement = document.querySelector('.home');
7 | const navbarElement = document.querySelector('header nav');
8 |
9 | const [_, figureElement, weddingToElement, openWeddingButton] = welcomeElement.children;
10 | const [audioMusic, audioButton] = document.querySelector('.audio').children;
11 | const [iconButton] = audioButton.children;
12 |
13 | const generateFigureContent = (bride) => {
14 | const {L: {name: brideLName}, P: {name: bridePName}, couple: coupleImage} = bride;
15 | return `
16 |
17 |
18 | ${brideLName.split(' ')[0]} & ${bridePName.split(' ')[0]}
19 | `;
20 | };
21 |
22 | const generateParameterContent = () => {
23 | const name = document.querySelector('#name');
24 | const params = getQueryParameter('to');
25 |
26 | if (params) {
27 | weddingToElement.innerHTML = `Kepada Yth Bapak/Ibu/Saudara/i${params} `;
28 | name.value = params;
29 | } else {
30 | weddingToElement.innerHTML = `Kepada Yth Bapak/Ibu/Saudara/iTeman-teman semua `;
31 | }
32 | }
33 |
34 | const initialAudio = () => {
35 | let isPlaying = false;
36 |
37 | audioMusic.innerHTML = ` `;
38 |
39 | audioButton.addEventListener('click', () => {
40 |
41 | if (isPlaying) {
42 | addClassElement(audioButton, 'active');
43 | removeClassElement(iconButton, 'bx-play-circle');
44 | addClassElement(iconButton, 'bx-pause-circle');
45 | audioMusic.play();
46 | } else {
47 | removeClassElement(audioButton, 'active');
48 | removeClassElement(iconButton, 'bx-pause-circle');
49 | addClassElement(iconButton, 'bx-play-circle');
50 | audioMusic.pause();
51 | }
52 | isPlaying = !isPlaying;
53 | });
54 | };
55 |
56 | openWeddingButton.addEventListener('click', () => {
57 | addClassElement(document.body, 'active');
58 | addClassElement(welcomeElement, 'hide');
59 |
60 | setTimeout(() => {
61 | addClassElement(homeElement, 'active');
62 | addClassElement(navbarElement, 'active');
63 | addClassElement(audioButton, 'show');
64 | removeClassElement(iconButton, 'bx-play-circle');
65 | addClassElement(iconButton, 'bx-pause-circle');
66 | audioMusic.play();
67 | }, 1500);
68 |
69 | setTimeout(() => {
70 | addClassElement(audioButton, 'active');
71 | }, 3000);
72 | });
73 |
74 | const initializeWelcome = () => {
75 | figureElement.innerHTML = generateFigureContent(data.bride);
76 | generateParameterContent();
77 | addClassElement(welcomeElement, 'active');
78 | }
79 |
80 | initializeWelcome();
81 | initialAudio();
82 | }
--------------------------------------------------------------------------------
/src/assets/data/data.js:
--------------------------------------------------------------------------------
1 | export const data = {
2 | bride: {
3 | L: {
4 | id: 1,
5 | name: 'Lorem Ipsum',
6 | child: 'Putra ke lorem',
7 | father: 'Lorem',
8 | mother: 'Ipsum',
9 | image: './src/assets/images/cowo.png'
10 | },
11 | P: {
12 | id: 2,
13 | name: 'Ipsum Lorem',
14 | child: 'Putri ke lorem',
15 | father: 'Lorem',
16 | mother: 'Ipsum',
17 | image: './src/assets/images/cewe.png'
18 | },
19 |
20 | couple: './src/assets/images/couple.png'
21 | },
22 |
23 | time: {
24 | marriage: {
25 | year: '2030',
26 | month: 'November',
27 | date: '14',
28 | day: 'Kamis',
29 | hours: {
30 | start: '08.00',
31 | finish: 'Selesai'
32 | }
33 | },
34 | reception: {
35 | year: '2024',
36 | month: 'November',
37 | date: '14',
38 | day: 'Kamis',
39 | hours: {
40 | start: '11.00',
41 | finish: 'Selesai'
42 | }
43 | },
44 | address: 'Kp. Lorem, RT 000/ RW 000, Desa.Lorem, Kec.Ipsum, Kab.Lorem, Lorem (1234)'
45 | },
46 |
47 | link: {
48 | calendar: 'https://calendar.app.google/oSVLRMYC79GzuA4f9',
49 | map: 'https://maps.app.goo.gl/q1Ask2Jgd4ekiiKBA',
50 | },
51 |
52 | galeri: [
53 | {
54 | id: 1,
55 | image: './src/assets/images/1.png'
56 | },
57 | {
58 | id: 2,
59 | image: './src/assets/images/2.png'
60 | },
61 | {
62 | id: 3,
63 | image: './src/assets/images/3.png'
64 | },
65 | {
66 | id: 4,
67 | image: './src/assets/images/4.png'
68 | },
69 | {
70 | id: 5,
71 | image: './src/assets/images/5.png'
72 | }
73 | ],
74 |
75 | bank: [
76 | {
77 | id: 1,
78 | name: 'Lorem Ipsum',
79 | icon: './src/assets/images/bca.png',
80 | rekening: '12345678'
81 | },
82 | {
83 | id: 2,
84 | name: 'Ipsum Lorem',
85 | icon: './src/assets/images/bri.png',
86 | rekening: '12345678'
87 | },
88 | ],
89 |
90 | audio: './src/assets/audio/wedding.mp3',
91 |
92 | api: 'https://script.google.com/macros/s/AKfycbyydz6N4p2VWUG8zsXeURv6ap9RP8a4eC3x6N3x6qTDjMVr1cIBz9S0NsHw2rWvBOSXGg/exec',
93 |
94 | navbar: [
95 | {
96 | id: 1,
97 | teks: 'Home',
98 | icon: 'bx bxs-home-heart',
99 | path: '#home',
100 | },
101 | {
102 | id: 2,
103 | teks: 'Mempelai',
104 | icon: 'bx bxs-group',
105 | path: '#bride',
106 | },
107 | {
108 | id: 3,
109 | teks: 'Tanggal',
110 | icon: 'bx bxs-calendar-check',
111 | path: '#time',
112 | },
113 | {
114 | id: 4,
115 | teks: 'Galeri',
116 | icon: 'bx bxs-photo-album',
117 | path: '#galeri',
118 | },
119 | {
120 | id: 5,
121 | teks: 'Ucapan',
122 | icon: 'bx bxs-message-rounded-dots',
123 | path: '#wishas',
124 | },
125 | ],
126 | }
127 |
--------------------------------------------------------------------------------
/src/css/mediaQueries.css:
--------------------------------------------------------------------------------
1 | @media screen and (max-width: 768px) {
2 | header nav {
3 | display: flex;
4 | }
5 |
6 | .home svg {
7 | bottom: -5rem;
8 | }
9 |
10 | .bride, .time{
11 | padding: 6rem 1.4rem 8rem;
12 | }
13 |
14 | .bride h2, .bride h3 {
15 | font-size: 1.8rem;
16 | }
17 |
18 | .bride i:nth-of-type(1) {
19 | right: 1.4rem;
20 | top: 2.6rem;
21 | }
22 |
23 | .bride i:nth-of-type(2) {
24 | left: 1.4rem;
25 | top: 24rem;
26 | }
27 |
28 | .bride i:nth-of-type(3) {
29 | right: 1.4rem;
30 | top: 55rem;
31 | }
32 |
33 | .bride i:nth-of-type(4) {
34 | left: 1.4rem;
35 | top: 60rem;
36 | }
37 |
38 | .bride i:nth-of-type(5) {
39 | right: 1.4rem;
40 | }
41 |
42 | .hadist {
43 | padding: 2rem 1.4rem;
44 | }
45 |
46 | .hadist i {
47 | left: 1.4rem;
48 | top: 24rem;
49 | }
50 |
51 | .hadist svg:nth-of-type(1) {
52 | top: -4rem;
53 | }
54 |
55 | .hadist svg:nth-of-type(2) {
56 | bottom: -5rem;
57 | }
58 |
59 | .time a + p + i {
60 | right: 1.4rem;
61 | top: 16rem;
62 | }
63 |
64 | .galeri {
65 | margin-top: -6rem;
66 | padding: 4rem 1.4rem;
67 | }
68 |
69 | .galeri .container {
70 | padding: 0 1rem 1rem;
71 | }
72 |
73 | .galeri .container figure {
74 | margin-top: -1rem;
75 | }
76 |
77 | .galeri .container figure img {
78 | height: 14rem;
79 | }
80 |
81 | .galeri .container ul {
82 | bottom: 5rem;
83 | }
84 |
85 | .galeri .container ul li {
86 | width: 1rem;
87 | height: 0.2rem;
88 | }
89 |
90 | .galeri .container button:nth-of-type(1),
91 | .galeri .container button:nth-of-type(2) {
92 | top: 12.4rem;
93 | font-size: 3rem;
94 | }
95 |
96 | .galeri .container button:nth-of-type(1) {
97 | left: 0.8rem;
98 | }
99 |
100 | .galeri .container button:nth-of-type(2){
101 | right: 0.8rem;
102 | }
103 |
104 | .galeri div:nth-of-type(2) {
105 | padding: 0 1rem 4rem;
106 | }
107 |
108 | .galeri div:nth-of-type(2) > div {
109 | margin-top: -1rem;
110 | padding: 2rem 1rem;
111 | grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
112 | gap: 1rem;
113 | }
114 |
115 | .galeri div:nth-of-type(2) > div img {
116 | border-radius: 15px;
117 | }
118 |
119 | .galeri div:nth-of-type(2) button {
120 | font-size: 1.4rem;
121 | }
122 |
123 | .galeri > i {
124 | top: 2rem;
125 | left: 3rem;
126 | }
127 |
128 | .wishas {
129 | padding: 1rem 1.4rem 2rem;
130 | margin-top: -14rem;
131 | }
132 |
133 | .wishas div:nth-of-type(1) > div{
134 | flex-direction: column;
135 | }
136 |
137 | .wishas div:nth-of-type(1) > div figure {
138 | width: 100%;
139 | }
140 |
141 | .wishas div:nth-of-type(1) > div figure img {
142 | width: 8rem;
143 | }
144 |
145 | .wishas div:nth-of-type(2) {
146 | padding: 2rem 1rem;
147 | }
148 |
149 | .wishas div:nth-of-type(2) > form {
150 | margin-top: -1rem;
151 | }
152 |
153 | .wishas div:nth-of-type(3) {
154 | padding: 2rem 1rem;
155 | }
156 |
157 | .wishas div:nth-of-type(3) ul li {
158 | gap: 0.6rem;
159 | }
160 |
161 | .wishas div:nth-of-type(3) .button-grup button {
162 | padding: 0.3rem 0.6rem;
163 | font-size: 0.8rem;
164 | }
165 |
166 | .wishas svg:nth-of-type(1){
167 | top: -4rem;
168 | }
169 |
170 | .wishas svg:nth-of-type(2) {
171 | bottom: -5rem;
172 | }
173 |
174 | .audio button {
175 | bottom: 5rem;
176 | right: 0.8rem;
177 | }
178 |
179 | footer {
180 | margin-top: 4rem;
181 | padding: 1rem 1.4rem 3.4rem;
182 | }
183 |
184 | footer h2 {
185 | font-size: 1.4rem;
186 | }
187 |
188 | footer > div a {
189 | font-size: 0.6rem;
190 | }
191 | }
--------------------------------------------------------------------------------
/src/css/galeri.css:
--------------------------------------------------------------------------------
1 | .galeri {
2 | width: 100%;
3 | min-height: 100vh;
4 | text-align: center;
5 | padding: 4rem 8rem 12rem;
6 | margin-top: -6rem;
7 | position: relative;
8 | }
9 |
10 | .galeri .container {
11 | width: 100%;
12 | padding: 0 1.5rem 2rem;
13 | border-radius: 50px;
14 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1), 0 -1px 1px rgba(0, 0, 0, 0.1);
15 | position: relative;
16 | }
17 |
18 | .galeri .container h2, .galeri div:nth-of-type(2) h2 {
19 | font-family: var(--sacramento);
20 | font-size: var(--fontSize-heading-base);
21 | font-weight: 400;
22 | padding: 2rem 1rem;
23 | }
24 |
25 | .galeri .container figure {
26 | width: 100%;
27 | margin: 0 auto;
28 | display: flex;
29 | align-items: center;
30 | justify-content: center;
31 | overflow-x: auto;
32 | scrollbar-width: none;
33 | }
34 |
35 | .galeri .container figure::-webkit-scrollbar,
36 | .galeri div:nth-of-type(2)::-webkit-scrollbar {
37 | display: none;
38 | }
39 |
40 | .galeri .container figure img {
41 | width: 100%;
42 | height: 35rem;
43 | object-fit: cover;
44 | border-radius: 15px;
45 | transition: var(--transition-small);
46 | }
47 |
48 | .galeri .container ul {
49 | padding: 0.5rem;
50 | display: flex;
51 | align-items: center;
52 | justify-content: center;
53 | gap: 0.4rem;
54 | position: absolute;
55 | left: 0;
56 | right: 0;
57 | bottom: 5.6rem;
58 | }
59 |
60 | .galeri .container ul li {
61 | width: 1.8rem;
62 | height: 0.25rem;
63 | background-color: rgba(255, 255, 255, 0.5);
64 | border-radius: 1px;
65 | list-style-type: none;
66 | cursor: pointer;
67 | }
68 |
69 | .galeri .container ul li:hover, .galeri .container ul li.active {
70 | background-color: #fff;
71 | }
72 |
73 | .galeri .container button:nth-of-type(1),
74 | .galeri .container button:nth-of-type(2) {
75 | font-size: 4rem;
76 | color: rgba(255, 255, 255, 0.5);
77 | background: none;
78 | border: none;
79 | position: absolute;
80 | top: 24.6rem;
81 | }
82 |
83 | .galeri .container button:nth-of-type(1) i:hover,
84 | .galeri .container button:nth-of-type(2) i:hover {
85 | color: #fff;
86 | cursor: pointer;
87 | }
88 |
89 | .galeri .container button:nth-of-type(1) {
90 | left: 2rem;
91 | }
92 |
93 | .galeri .container button:nth-of-type(2) {
94 | right: 2rem;
95 | }
96 |
97 | .galeri .container button:nth-of-type(3) {
98 | padding: 0.5rem 1rem;
99 | background-color: #fff;
100 | border: 1px solid #000;
101 | border-radius: 20px;
102 | margin-top: 1rem;
103 | transition: var(--transition-small);
104 | }
105 |
106 | .galeri .container button:nth-of-type(3):hover {
107 | color: #fff;
108 | background-color: #000;
109 | cursor: pointer;
110 | }
111 |
112 | .galeri div:nth-of-type(2) {
113 | width: 100%;
114 | height: 100vh;
115 | padding: 2rem 8rem;
116 | position: fixed;
117 | top: 0;
118 | left: 0;
119 | right: 0;
120 | bottom: 0;
121 | background-color: #fff;
122 | z-index: 99;
123 | overflow-y: auto;
124 | transition: all .3s ease;
125 | transform: scale(0);
126 | opacity: 0;
127 | transform-origin: bottom;
128 | }
129 |
130 | .galeri div:nth-of-type(2) > div {
131 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1), 0 -1px 1px rgba(0, 0, 0, 0.1);
132 | border-radius: 20px;
133 | padding: 2rem;
134 | margin-top: 1rem;
135 | display: grid;
136 | grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
137 | gap: 2rem;
138 | }
139 |
140 | .galeri div:nth-of-type(2) > div img {
141 | width: 100%;
142 | object-fit: cover;
143 | border-radius: 20px;
144 | }
145 |
146 | .galeri div:nth-of-type(2) button {
147 | padding:0;
148 | font-size: 2rem;
149 | background:none;
150 | border: none;
151 | cursor: pointer;
152 | position: absolute;
153 | top: 1rem;
154 | right: 1rem;
155 | transition: var(--transition-small);
156 | }
157 |
158 | .galeri div:nth-of-type(2) button:hover i {
159 | text-shadow: 1px 2px 5px rgba(0,0,0,0.5);
160 | }
161 |
162 | .galeri div:nth-of-type(2).active {
163 | transform: scale(1);
164 | opacity: 1;
165 | }
166 |
167 | .galeri > i {
168 | top:2rem;
169 | left: 10rem;
170 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Fitur ✨
2 |
3 | - 📊 **Penyimpanan Data**: Menggunakan Google Sheets API untuk menyimpan dan mengelola data undangan.
4 | - 📱 **Desain Responsif**: Dioptimalkan untuk berbagai ukuran layar.
5 |
6 | ## Teknologi yang Digunakan 🛠️
7 |
8 | 
9 | 
10 | 
11 | 
12 | 
13 |
14 | ---
15 |
16 | ## Cara Menggunakan 🚀
17 |
18 | 1. **Unduh proyek atau kloning repositori**:
19 | ```bash
20 | git clone https://github.com/sandiperdiansah/wedding-invitation.git
21 | ```
22 |
23 | 2. **Catatan**: Anda memiliki dua pilihan untuk menggunakan kode sumber ini:
24 | - **Tanpa Modifikasi**: Buka file [data.js](https://github.com/SandiPerdiansah/wedding-invitation/blob/main/src/assets/data/data.js) untuk langsung menggunakannya.
25 | - **Dengan Kustomisasi**: Sesuaikan tampilan dan kontennya sesuai kebutuhan Anda.
26 |
27 | ### Cara Mengedit `data.js` ✏️
28 |
29 | - **Detail**: Perbarui nama pengantin, tanggal dan waktu pernikahan, serta alamat lokasi acara.
30 | - **Tautan**:
31 | - 📅 **Kalender**: Tambahkan URL acara di Google Calendar. Buat acara, tambahkan judul dan deskripsi, lalu simpan. Klik "Bagikan" untuk mendapatkan tautan yang dapat dibagikan.
32 |
33 | 
34 | 
35 | 
36 |
37 | - 📍 **Peta**: Masukkan URL lokasi acara dari Google Maps.
38 | - 🖼️ **Galeri**: Anda bisa menambahkan lebih dari 5 gambar, pastikan ukurannya dioptimalkan untuk web.
39 | - 🎵 **Audio**: Ganti file audio default dan pastikan ukurannya optimal.
40 |
41 | ### Mengatur Google Sheets API 📜
42 |
43 | 1. **Buat file Google Sheets**: Buka Google Sheets, buat file baru, dan beri nama sesuai keinginan.
44 |
45 | 
46 |
47 | 2. **Atur Tabel di Google Sheets**:
48 | 
49 | Struktur tabel: (id, nama, status, pesan, tanggal, warna)
50 |
51 | 4. **Tambahkan Google Apps Script**:
52 | - Buka file Google Sheets, masuk ke "Ekstensi" > "Apps Script", lalu tambahkan kode berikut:
53 |
54 | ```javascript
55 | const SHEET_NAME = 'comentar';
56 |
57 | const doGet = () => {
58 | try {
59 | const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
60 | const [, ...data] = sheet.getDataRange().getValues();
61 |
62 | const comentar = data.map(([id, name, status, message, date, color]) => ({
63 | id,
64 | name,
65 | status,
66 | message,
67 | date,
68 | color
69 | }));
70 |
71 | const response = {
72 | status: 200,
73 | message: 'Berhasil mengambil data',
74 | comentar
75 | };
76 |
77 | return ContentService
78 | .createTextOutput(JSON.stringify(response))
79 | .setMimeType(ContentService.MimeType.JSON);
80 | } catch (error) {
81 | return ContentService
82 | .createTextOutput(JSON.stringify({ status: 500, message: `Kesalahan: ${error}` }))
83 | .setMimeType(ContentService.MimeType.JSON);
84 | }
85 | };
86 |
87 | const doPost = (e) => {
88 | try {
89 | const { id, name, status, message, date, color } = JSON.parse(e.postData.contents);
90 | const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
91 |
92 | if (!sheet) {
93 | throw new Error(`Sheet "${SHEET_NAME}" tidak ditemukan`);
94 | }
95 |
96 | sheet.appendRow([id, name, status, message, date, color]);
97 |
98 | return ContentService
99 | .createTextOutput(JSON.stringify({ status: 200, message: 'Data berhasil ditambahkan' }))
100 | .setMimeType(ContentService.MimeType.JSON);
101 | } catch (error) {
102 | return ContentService
103 | .createTextOutput(JSON.stringify({ status: 500, message: `Kesalahan: ${error}` }))
104 | .setMimeType(ContentService.MimeType.JSON);
105 | }
106 | };
107 | ```
108 |
109 | 5. **Deploy Script**:
110 | - Klik "Deploy" lalu pilih "Deployment Baru".
111 | - Pilih "Web app" dalam pengaturan deployment.
112 |
113 | 
114 | 
115 | 
116 |
117 | Salin URL yang diberikan. Jika terjadi kesalahan, coba deploy ulang karena terkadang ada masalah dengan cross-origin pada percobaan pertama.
118 |
119 | ---
120 |
121 | ## Penghargaan Desain 🎨
122 |
123 | Proyek ini merupakan redesain dari undangan pernikahan milik [Dewanakl](https://github.com/dewanakl). Tata letak dan desain telah disesuaikan untuk meningkatkan fungsi dan estetika.
124 |
125 | ---
126 |
127 | Terima kasih telah mengunjungi repositori ini. Semoga hari pernikahan Anda berjalan lancar dan penuh kebahagiaan! 😊
128 |
--------------------------------------------------------------------------------
/src/css/wishas.css:
--------------------------------------------------------------------------------
1 | .wishas {
2 | width: 100%;
3 | min-height: 100vh;
4 | padding: 4rem 8rem;
5 | background-color: var(--bg-color);
6 | position: relative;
7 | text-align: center;
8 | }
9 |
10 | .wishas div:nth-of-type(1) h2, .wishas div:nth-of-type(2) h2 {
11 | font-size: var(--fontSize-heading-base);
12 | font-family: var(--sacramento);
13 | font-weight: 400;
14 | }
15 |
16 | .wishas div:nth-of-type(1) > div {
17 | margin-top: 1rem;
18 | width: 100%;
19 | display: flex;
20 | align-items: center;
21 | justify-content: center;
22 | gap: 1rem;
23 | }
24 |
25 | .wishas div:nth-of-type(1) > div figure {
26 | width: 24rem;
27 | text-align: center;
28 | border-radius: 15px;
29 | padding: 1rem 0;
30 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1), 0 -1px 1px rgba(0, 0, 0, 0.1);
31 | }
32 |
33 | .wishas div:nth-of-type(1) > div figure img {
34 | width: 10rem;
35 | height: 3rem;
36 | object-fit: contain;
37 | margin-bottom: 1rem;
38 | }
39 |
40 | .wishas div:nth-of-type(1) > div figure figcaption {
41 | line-height: 1.8rem;
42 | margin-bottom: 1rem;
43 | }
44 |
45 | .wishas div:nth-of-type(1) > div figure button {
46 | padding: 0.4rem 1rem;
47 | border: 1px solid #000;
48 | background-color: #fff;
49 | border-radius: 10px;
50 | cursor: pointer;
51 | transition: var(--transition-small);
52 | }
53 |
54 | .wishas div:nth-of-type(1) > div figure button:hover {
55 | background-color: #000;
56 | color: #fff;
57 | }
58 |
59 | .wishas div:nth-of-type(2){
60 | margin-top: 2rem;
61 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1), 0 -1px 1px rgba(0, 0, 0, 0.1);
62 | border-radius: 15px;
63 | padding: 2rem 4rem 4rem;
64 | }
65 |
66 | .wishas div:nth-of-type(2) > form {
67 | margin-top: 1rem;
68 | width: 100%;
69 | display: flex;
70 | align-items: start;
71 | justify-content: start;
72 | flex-direction: column;
73 | }
74 |
75 | .wishas div:nth-of-type(2) > form label {
76 | display: block;
77 | margin-top: 1.4rem;
78 | margin-bottom: 4px;
79 | }
80 |
81 | .wishas div:nth-of-type(2) > form input,
82 | .wishas div:nth-of-type(2) > form select,
83 | .wishas div:nth-of-type(2) > form textarea {
84 | width: 100%;
85 | padding: 0.6rem 1rem;
86 | border-radius: 5px;
87 | border: 1px solid #000;
88 | }
89 |
90 | .wishas div:nth-of-type(2) > form textarea {
91 | min-height: 6rem;
92 | }
93 |
94 | .wishas div:nth-of-type(2) > form button {
95 | width: 100%;
96 | color: #fff;
97 | background-color: #000;
98 | padding: 0.6rem 0;
99 | border-radius: 5px;
100 | margin-top: 1rem;
101 | border: none;
102 | cursor: pointer;
103 | }
104 |
105 | .wishas div:nth-of-type(3) {
106 | margin-top: 2rem;
107 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1), 0 -1px 1px rgba(0, 0, 0, 0.1);
108 | border-radius: 15px;
109 | padding: 2rem 4rem;
110 | }
111 |
112 | .wishas div:nth-of-type(3) ul {
113 | margin-top: 1.4rem;
114 | width: 100%;
115 | display: flex;
116 | align-items: start;
117 | justify-content: start;
118 | flex-direction: column;
119 | gap: 1rem;
120 | min-height: 26rem;
121 | max-height: 30rem;
122 | overflow-y: hidden;
123 | }
124 |
125 | .wishas div:nth-of-type(3) ul li {
126 | display: flex;
127 | align-items: start;
128 | justify-content: start;
129 | gap: 1rem;
130 | }
131 |
132 | .wishas div:nth-of-type(3) ul li div:nth-of-type(1){
133 | width: 2rem;
134 | height: 2rem;
135 | border-radius: 50%;
136 | display: flex;
137 | align-items: center;
138 | justify-content: center;
139 | flex-shrink: 0;
140 | color: #fff;
141 | }
142 |
143 | .wishas div:nth-of-type(3) ul li div:nth-of-type(2){
144 | margin: 0.4rem 0 0;
145 | padding: 0;
146 | text-align: start;
147 | box-shadow: none;
148 | }
149 |
150 | .wishas div:nth-of-type(3) ul li div:nth-of-type(2) h4 {
151 | font-size: 1rem;
152 | font-weight: 400;
153 | }
154 |
155 | .wishas div:nth-of-type(3) ul li div:nth-of-type(2) p:nth-of-type(1){
156 | font-size: 0.6rem;
157 | font-weight: 300;
158 | line-height: 1rem;
159 | }
160 |
161 | .wishas div:nth-of-type(3) ul li div:nth-of-type(2) p:nth-of-type(2){
162 | margin-top: 0.4rem;
163 | font-weight: 300;
164 | font-size: 0.8rem;
165 | }
166 |
167 | .wishas div:nth-of-type(3) .button-grup {
168 | margin: 1rem auto 0;
169 | border: 1px solid #000;
170 | border-radius: 4px;
171 | background-color: #fff;
172 | width: fit-content;
173 | display: flex;
174 | align-items: center;
175 | justify-content: center;
176 | }
177 |
178 | .wishas div:nth-of-type(3) .button-grup > span {
179 | background-color: #d5d2d2;
180 | width: 2rem;
181 | height: 2rem;
182 | display: flex;
183 | align-items: center;
184 | justify-content: center;
185 | }
186 |
187 | .wishas div:nth-of-type(3) .button-grup button {
188 | padding: 0.5rem 1rem;
189 | border: none;
190 | background:none;
191 | cursor: pointer;
192 | display: flex;
193 | align-items: center;
194 | justify-content: center;
195 | gap: 0.4rem;
196 | }
197 |
198 | .wishas svg:nth-of-type(1){
199 | position: absolute;
200 | z-index: -10;
201 | top: -12rem;
202 | left: 0;
203 | right: 0;
204 | }
205 |
206 | .wishas svg:nth-of-type(2) {
207 | position: absolute;
208 | z-index: -10;
209 | left: 0;
210 | right: 0;
211 | bottom: -15rem;
212 | }
--------------------------------------------------------------------------------
/src/js/wishas.js:
--------------------------------------------------------------------------------
1 | import {
2 | formattedDate,
3 | formattedName,
4 | generateRandomColor,
5 | generateRandomId,
6 | getCurrentDateTime,
7 | renderElement
8 | } from "../utils/helper.js";
9 | import {data} from "../assets/data/data.js";
10 | import {comentarService} from "../services/comentarService.js";
11 |
12 | export const wishas = () => {
13 | const wishasContainer = document.querySelector('.wishas');
14 | const [_, form] = wishasContainer.children[2].children;
15 | const [peopleComentar, ___, containerComentar] = wishasContainer.children[3].children;
16 | const buttonForm = form.children[6];
17 | const pageNumber = wishasContainer.querySelector('.page-number');
18 | const [prevButton, nextButton] = wishasContainer.querySelectorAll('.button-grup button');
19 |
20 | const listItemBank = (data) => (
21 | `
22 |
23 | No. Rekening ${data.rekening.slice(0, 4)}xxxx A.n ${data.name}
24 | Salin No. Rekening
25 | `
26 | );
27 |
28 | const initialBank = () => {
29 | const wishasBank = wishasContainer.children[1];
30 | const [_, __, containerBank] = wishasBank.children;
31 |
32 | renderElement(data.bank, containerBank, listItemBank);
33 |
34 | containerBank.querySelectorAll('button').forEach((button) => {
35 | button.addEventListener('click', async (e) => {
36 | const rekening = e.target.dataset.rekening;
37 | try {
38 | await navigator.clipboard.writeText(rekening);
39 | button.textContent = 'Berhasil menyalin';
40 | } catch (error) {
41 | console.log(`Error : ${error.message}`);
42 | } finally {
43 | setTimeout(() => {
44 | button.textContent = 'Salin No. Rekening';
45 | }, 2000);
46 | }
47 | });
48 | });
49 | };
50 |
51 | const listItemComentar = (data) => {
52 | const name = formattedName(data.name);
53 | const newDate = formattedDate(data.date);
54 | let date = "";
55 |
56 | if (newDate.days < 1) {
57 | if (newDate.hours < 1) {
58 | date = `${newDate.minutes} menit yang lalu`;
59 | } else {
60 | date = `${newDate.hours} jam, ${newDate.minutes} menit yang lalu`;
61 | }
62 | } else {
63 | date = `${newDate.days} hari, ${newDate.hours} jam yang lalu`;
64 | }
65 |
66 | return `
67 | ${data.name.charAt(0).toUpperCase()}
68 |
69 |
${name}
70 |
${date} ${data.status}
71 |
${data.message}
72 |
73 | `;
74 | };
75 |
76 | let lengthComentar;
77 |
78 | const initialComentar = async () => {
79 | containerComentar.innerHTML = `Loading... `;
80 | peopleComentar.textContent = '...';
81 | pageNumber.textContent = '..';
82 |
83 | try {
84 | const response = await comentarService.getComentar();
85 | const {comentar} = response;
86 |
87 | lengthComentar = comentar.length;
88 | comentar.reverse();
89 |
90 | if (comentar.length > 0) {
91 | peopleComentar.textContent = `${comentar.length} Orang telah mengucapkan`;
92 | } else {
93 | peopleComentar.textContent = `Belum ada yang mengucapkan`;
94 | }
95 |
96 | pageNumber.textContent = '1';
97 | renderElement(comentar.slice(startIndex, endIndex), containerComentar, listItemComentar);
98 | } catch (error) {
99 | return `Error : ${error.message}`;
100 | }
101 | };
102 |
103 | form.addEventListener('submit', async (e) => {
104 | e.preventDefault();
105 | buttonForm.textContent = 'Loading...';
106 |
107 | const comentar = {
108 | id: generateRandomId(),
109 | name: e.target.name.value,
110 | status: e.target.status.value === 'y' ? 'Hadir' : 'Tidak Hadir',
111 | message: e.target.message.value,
112 | date: getCurrentDateTime(),
113 | color: generateRandomColor(),
114 | };
115 |
116 | try {
117 | const response = await comentarService.getComentar();
118 |
119 | await comentarService.addComentar(comentar);
120 |
121 | lengthComentar = response.comentar.length;
122 |
123 | peopleComentar.textContent = `${++response.comentar.length} Orang telah mengucapkan`;
124 | containerComentar.insertAdjacentHTML('afterbegin', listItemComentar(comentar));
125 | } catch (error) {
126 | return `Error : ${error.message}`;
127 | } finally {
128 | buttonForm.textContent = 'Kirim';
129 | form.reset();
130 | }
131 | });
132 |
133 | // click prev & next
134 | let currentPage = 1;
135 | let itemsPerPage = 4;
136 | let startIndex = 0;
137 | let endIndex = itemsPerPage;
138 |
139 | const updatePageContent = async () => {
140 | containerComentar.innerHTML = 'Loading... ';
141 | pageNumber.textContent = '..';
142 | prevButton.disabled = true;
143 | nextButton.disabled = true;
144 |
145 | try {
146 | const response = await comentarService.getComentar();
147 | const {comentar} = response;
148 |
149 | comentar.reverse();
150 |
151 | renderElement(comentar.slice(startIndex, endIndex), containerComentar, listItemComentar);
152 | pageNumber.textContent = currentPage.toString();
153 | } catch (error) {
154 | console.log(error);
155 | } finally {
156 | prevButton.disabled = false;
157 | nextButton.disabled = false;
158 | }
159 | }
160 |
161 | nextButton.addEventListener('click', async () => {
162 | if (endIndex <= lengthComentar) {
163 | currentPage++;
164 | startIndex = (currentPage - 1) * itemsPerPage;
165 | endIndex = startIndex + itemsPerPage;
166 | await updatePageContent();
167 | }
168 | });
169 |
170 | prevButton.addEventListener('click', async () => {
171 | if (currentPage > 1) {
172 | currentPage--;
173 | startIndex = (currentPage - 1) * itemsPerPage;
174 | endIndex = startIndex + itemsPerPage;
175 | await updatePageContent();
176 | }
177 | });
178 |
179 | initialComentar().then();
180 | initialBank();
181 | };
182 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | wedding - invitation
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 | The Wedding Of
70 |
71 |
72 |
73 |
74 | Buka Undangan
75 |
76 |
77 |
78 |
97 |
98 |
99 | بِسْمِ اللّٰهِ الرَّحْمٰنِ الرَّحِيْمِ
100 | Assalamualaikum Warahmatullahi Wabarakatuh
101 |
102 | Tanpa mengurangi rasa hormat. Kami mengundang Bapak/Ibu/Saudara/i serta kerabat sekalian untuk menghadiri
103 | acara pernikahan kami:
104 |
105 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
119 |
120 | Allah Subhanahu Wa Ta'ala berfirman
121 |
122 | Dan di antara tanda-tanda (kebesaran)-Nya ialah Dia menciptakan pasangan-pasangan untukmu dari jenismu
123 | sendiri, agar kamu cenderung dan merasa tenteram kepadanya, dan Dia menjadikan di antaramu rasa kasih dan
124 | sayang. Sungguh, pada yang demikian itu benar-benar terdapat tanda-tanda (kebesaran Allah) bagi kaum yang
125 | berpikir.QS. Ar-Rum Ayat 21
126 |
127 |
128 |
129 |
131 |
132 |
133 |
134 |
135 |
136 | Waktu & Tempat
137 |
138 | Dengan memohon rahmat dan ridho Allah Subhanahu Wa Ta'ala, insyaAllah kami akan menyelenggarakan acara :
139 |
140 |
144 |
146 |
147 | Lihat google maps
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
Galeri
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
Lihat semua foto
165 |
166 |
167 |
Galeri
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
180 |
181 |
182 |
Love Gift
183 |
184 | Tanpa mengurangi rasa hormat, bagi anda yang ingin memberikan tanda kasih untuk kami, dapat melalui
185 | :
186 |
187 |
188 |
189 |
190 |
191 |
Ucapan & Doa
192 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 | prev
223 |
224 |
225 |
226 | next
227 |
228 |
229 |
230 |
231 |
232 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 | Merupakan suatu kebahagiaan dan kehormatan bagi kami, apabila Bapak/Ibu/Saudara/i, berkenan hadir dan memberikan
248 | do'a restu kepada kedua mempelai.
249 | Wassalamualaikum Warahmatullahi Wabarakatuh
250 | اَلْحَمْدُ للَّهِ رَبِّ الْعالَمِينَ
251 |
258 |
259 |
260 |
261 |
262 |
263 |
--------------------------------------------------------------------------------