Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c92a9c760 | |||
| 468bdf817a | |||
| b2139ed440 | |||
| 525e116489 | |||
| 61657b95e8 | |||
| cc8e21c430 | |||
| 0f92159735 | |||
| 0eb142696a | |||
| ed5128f75d | |||
| 02a2c17531 | |||
| 6cde2363ba | |||
| 28758f11b0 | |||
| b2cd7f3a14 | |||
| 5b224f8c83 | |||
| 93df670182 | |||
| 24fadc8add | |||
| 9883a7e798 | |||
| 5ada2fb58f | |||
| 0f013e0949 | |||
| 328cc2eae6 | |||
| 974db928c7 | |||
| 9d95049976 | |||
| d8cafdbdf0 | |||
| 7716257c5b | |||
| 869d2d5199 | |||
| ea0b339d69 | |||
| 518ba194ae | |||
| 10fdda3ec1 | |||
| bbfe68c533 | |||
| 1a312fb7a3 | |||
| 2b836f081c | |||
| 97ccd99b75 | |||
| 95f4cc4e6e | |||
| 236585afb1 | |||
| 5ad65b75da | |||
| ee453bf652 | |||
| 39359d7066 | |||
| 0b3a18d75a | |||
| 9b9e0a5466 | |||
| 8f6b8c99a9 | |||
| f945cdd58f | |||
| edde30b539 | |||
| 28fe9c29df | |||
| 289ec8b067 | |||
| 6ee981a63c | |||
| cf4e9a743a | |||
| b92e7cfd63 | |||
| a0f2603b3a | |||
| 922ca80d23 | |||
| 621398db24 | |||
| a8b2bf2c38 | |||
| 5a0b68c9a9 | |||
| b04bf1ea73 | |||
| 4953909070 |
@@ -13,7 +13,7 @@ jobs:
|
||||
run: |
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > /tmp/id_fin
|
||||
chmod 600 /tmp/id_fin
|
||||
#ссылка на репо
|
||||
#ключ машины
|
||||
- name: Create .env file
|
||||
run: echo "${{ secrets.RUNNER_ENV }}" > .env
|
||||
#env для runners
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
sparse-checkout: |
|
||||
ansible/
|
||||
- name: Run Ansible playbook
|
||||
run: ansible-playbook -i inventory.ini tmp-repo/ansible/deploy.yml -e @secrets.yml -e env_file="$(pwd)/.env"
|
||||
run: ansible-playbook -i tmp-repo/ansible/inventory.ini tmp-repo/ansible/deploy.yml -e @tmp-repo/ansible/secrets.yml -e env_file="$(pwd)/.env"
|
||||
env:
|
||||
ANSIBLE_PRIVATE_KEY_FILE: /tmp/id_fin
|
||||
ANSIBLE_HOST_KEY_CHECKING: "False"
|
||||
@@ -39,29 +39,18 @@ jobs:
|
||||
|
||||
- name: Create .env file
|
||||
run: echo "${{ secrets.WEDDING_SITE_ENV }}" > .env
|
||||
- name: Check env
|
||||
run: cat .env
|
||||
#env для проекта
|
||||
- name: Build image
|
||||
run: docker build -t back:latest -f docker/dockerfile .
|
||||
run: docker build -t back:latest -f docker/dockerfile.project .
|
||||
|
||||
- name: Start docker-compose of project
|
||||
run: docker compose --env-file .env -f docker/docker-compose.yaml up -d
|
||||
|
||||
- name: Checkout only caddy.yml
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: MH.Dmitrii/wedding-site
|
||||
ref: main
|
||||
path: caddy
|
||||
sparse-checkout: |
|
||||
docker/caddy/
|
||||
- name: Checkout web-site
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: MH.Dmitrii/wedding-site
|
||||
ref: main
|
||||
path: caddy/site
|
||||
sparse-checkout: |
|
||||
server/frontend
|
||||
- name: Start docker-compose caddy
|
||||
run: docker compose -f caddy/caddy.yaml up -d
|
||||
- name: Build image caddy
|
||||
run: docker build -t caddy:wedding -f docker/caddy/dockerfile.caddy .
|
||||
|
||||
- name: Start docker-compose caddy
|
||||
run: docker compose --env-file .env -f docker/caddy/caddy.yaml up -d
|
||||
|
||||
1
.gitignore
vendored
@@ -24,5 +24,4 @@ hint.py
|
||||
*.env
|
||||
#db
|
||||
*.db
|
||||
versions/
|
||||
data/
|
||||
@@ -1,9 +1,10 @@
|
||||
- hosts: servers
|
||||
- name: Preflight checks and deploy wedding-site
|
||||
hosts: servers
|
||||
become: yes
|
||||
vars:
|
||||
env_file: $(pwd)/.env
|
||||
tasks:
|
||||
|
||||
tasks:
|
||||
- name: Install wget
|
||||
apt:
|
||||
name: wget
|
||||
|
||||
@@ -1 +1 @@
|
||||
gitea_instance_url: https://git.homyk.space/MH.Dmitrii/wedding-site/src/branch/main/docker/gitea_runners.git
|
||||
gitea_instance_url: https://git.homyk.space/MH.Dmitrii/wedding-site/raw/branch/main/docker/gitea_runners/node-docker.yaml
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:<version>
|
||||
image: caddy:wedding
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
volumes:
|
||||
- $PWD/conf:/etc/caddy
|
||||
- $PWD/site:/srv
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
networks:
|
||||
- wedding-site-network
|
||||
- docker_wedding-site-network
|
||||
healthcheck:
|
||||
test: "curl -f https://ru.homyk.space"
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
retries: 1
|
||||
environment:
|
||||
- PORT=${PORT}
|
||||
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
networks:
|
||||
wedding-site-network:
|
||||
docker_wedding-site-network :
|
||||
external: true
|
||||
41
docker/caddy/conf/Caddyfile
Normal file
@@ -0,0 +1,41 @@
|
||||
http://ru.homyk.space {
|
||||
redir https://ru.homyk.space{uri} permanent
|
||||
}
|
||||
https://ru.homyk.space {
|
||||
encode gzip
|
||||
|
||||
# --- API ---
|
||||
handle /api/* {
|
||||
reverse_proxy backend:{$PORT}
|
||||
}
|
||||
|
||||
# --- Frontend ---
|
||||
root * /srv
|
||||
file_server
|
||||
|
||||
log {
|
||||
output file /data/logs/caddy.log {
|
||||
roll_size 5mb
|
||||
roll_keep 5
|
||||
roll_keep_for 72h
|
||||
}
|
||||
format json
|
||||
}
|
||||
}
|
||||
https://music.homyk.space {
|
||||
encode gzip
|
||||
|
||||
# --- API ---
|
||||
handle {
|
||||
reverse_proxy koel:80
|
||||
}
|
||||
|
||||
log {
|
||||
output file /data/logs/caddy1.log {
|
||||
roll_size 5mb
|
||||
roll_keep 5
|
||||
roll_keep_for 72h
|
||||
}
|
||||
format json
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
ru.homyk.space {
|
||||
|
||||
encode gzip
|
||||
|
||||
# --- API ---
|
||||
handle_path /api/* {
|
||||
reverse_proxy backend:8000
|
||||
}
|
||||
|
||||
# --- Frontend ---
|
||||
root * /srv
|
||||
file_server
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format console
|
||||
}
|
||||
}
|
||||
3
docker/caddy/dockerfile.caddy
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM caddy:latest
|
||||
COPY ./docker/caddy/conf/Caddyfile /etc/caddy/Caddyfile
|
||||
COPY ./server/frontend /srv
|
||||
@@ -3,15 +3,16 @@ services:
|
||||
image: back:latest
|
||||
container_name: wedding-site
|
||||
volumes:
|
||||
- wedding_volume:/home/backend
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- "${PORT}:${PORT}"
|
||||
- /home/sevice/DB:/home/backend/server/backend/database/DB
|
||||
expose:
|
||||
- "${PORT}"
|
||||
networks:
|
||||
- wedding-site-network
|
||||
|
||||
healthcheck:
|
||||
test: "curl -f http://localhost:${PORT}/docs"
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
retries: 1
|
||||
networks:
|
||||
wedding-site-network:
|
||||
driver: bridge
|
||||
volumes:
|
||||
wedding_volume: {}
|
||||
@@ -1,7 +1,10 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /home/backend
|
||||
COPY ../ /home/backend
|
||||
|
||||
RUN python -m pip install --upgrade pip \
|
||||
&& python -m pip install -r requirements.txt
|
||||
|
||||
RUN chmod +x ./docker/start.sh
|
||||
ENTRYPOINT ["./docker/start.sh"]
|
||||
@@ -5,5 +5,4 @@ RUN apt-get update && apt-get install -y \
|
||||
ansible \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Проверка версий
|
||||
RUN node -v && npm -v && ansible --version
|
||||
@@ -1,18 +1,15 @@
|
||||
# Dockerfile.runner
|
||||
FROM node:20-bullseye
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
lsb-release \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Добавляем репозиторий Docker
|
||||
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" \
|
||||
> /etc/apt/sources.list.d/docker.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y docker-ce-cli docker-compose-plugin
|
||||
|
||||
# Проверка версий
|
||||
RUN node -v && npm -v && docker -v && docker compose version
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/bin/sh
|
||||
alembic -c server/backend/database/alembic/alembic.ini revision --autogenerate
|
||||
alembic -c server/backend/database/alembic/alembic.ini upgrade head
|
||||
exec python run.py
|
||||
exec python run.py --user_name admin
|
||||
6
makefile
@@ -1,6 +1,6 @@
|
||||
VENV=source ./.venv/bin/activate;
|
||||
ALEMBIC=alembic -c ./server/backend/database/alembic/alembic.ini
|
||||
.PHONY: run run_debug migrate_head migrate_down migrate_history migrate_current migrate
|
||||
.PHONY: run run_debug migrate_head migrate_down migrate_history migrate_current migrate_heads migrate_stamp migrate
|
||||
run:
|
||||
$(VENV) python run.py --user_name admin
|
||||
run_debug:
|
||||
@@ -13,5 +13,9 @@ migrate_history:
|
||||
$(VENV) $(ALEMBIC) history
|
||||
migrate_current:
|
||||
$(VENV) $(ALEMBIC) current
|
||||
migrate_heads:
|
||||
$(VENV) $(ALEMBIC) heads
|
||||
migrate_stamp:
|
||||
$(VENV) $(ALEMBIC) stamp head
|
||||
migrate:
|
||||
$(VENV) $(ALEMBIC) revision --autogenerate
|
||||
70
run.py
@@ -1,46 +1,52 @@
|
||||
from server.backend.schema.pydantic import settings
|
||||
from server.backend.schema.pydantic import settings, UserCreate
|
||||
from server.backend.database.db import create_user, list_users
|
||||
from server.backend.schema.pydantic import UserCreate
|
||||
import asyncio
|
||||
import uvicorn
|
||||
def start(log_level:str):
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"server.backend.endpoints.endpoints:api",
|
||||
host="0.0.0.0",
|
||||
port=settings.PORT,
|
||||
reload=True,
|
||||
log_level=log_level,
|
||||
access_log=True
|
||||
)
|
||||
import asyncio
|
||||
import argparse
|
||||
|
||||
async def init_admin(user_name: str):
|
||||
admin_user = {
|
||||
"code": "123456",
|
||||
"name": user_name,
|
||||
"middlename": user_name,
|
||||
"surname": user_name,
|
||||
"admin": True
|
||||
}
|
||||
|
||||
users = await list_users() or []
|
||||
label = any(u.admin for u in users)
|
||||
|
||||
if not label:
|
||||
await create_user(UserCreate(**admin_user))
|
||||
|
||||
def start(log_level: str):
|
||||
uvicorn.run(
|
||||
"server.backend.endpoints.endpoints:api",
|
||||
host="0.0.0.0",
|
||||
port=settings.PORT,
|
||||
reload=False,
|
||||
log_level=log_level,
|
||||
access_log=True
|
||||
)
|
||||
|
||||
parser = argparse.ArgumentParser(description="logging and admin creation")
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["debug","info"],
|
||||
choices=["debug", "info"],
|
||||
default="info",
|
||||
help="Режим логирования (по умолчанию: info)"
|
||||
help="Режим логирования"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--user_name",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Создание первого пользователя)"
|
||||
help="Создание первого пользователя"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
async def arguments(args):
|
||||
admin_user = {
|
||||
"code": "123456",
|
||||
"name": args.user_name,
|
||||
"surname": args.user_name,
|
||||
"admin": True
|
||||
}
|
||||
users = await list_users()
|
||||
label = any(u.admin for u in users)
|
||||
if not label:
|
||||
await create_user(UserCreate(**admin_user))
|
||||
match args.mode:
|
||||
case "debug" | "info":
|
||||
print("Режим:", args.mode)
|
||||
start(args.mode)
|
||||
asyncio.run(arguments(args))
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(init_admin(args.user_name))
|
||||
|
||||
print("Режим:", args.mode)
|
||||
|
||||
start(args.mode)
|
||||
@@ -0,0 +1,32 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 15b87c1584bf
|
||||
Revises: 2e39b25a3b28
|
||||
Create Date: 2026-03-13 22:53:40.498042
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '15b87c1584bf'
|
||||
down_revision: Union[str, Sequence[str], None] = 'dd476c0dcf61'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'food')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('food', sa.BOOLEAN(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 1e2bd98e74a5
|
||||
Revises: 4ffe643b7d40
|
||||
Create Date: 2026-03-07 00:44:25.427515
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '1e2bd98e74a5'
|
||||
down_revision: Union[str, Sequence[str], None] = '4ffe643b7d40'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 1fa13c2c4df4
|
||||
Revises: 15b87c1584bf
|
||||
Create Date: 2026-03-13 22:56:36.115487
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '1fa13c2c4df4'
|
||||
down_revision: Union[str, Sequence[str], None] = '15b87c1584bf'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('type_of_food', sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'type_of_food')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 4ffe643b7d40
|
||||
Revises: 965a4fb49fae
|
||||
Create Date: 2026-03-07 00:21:36.656185
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '4ffe643b7d40'
|
||||
down_revision: Union[str, Sequence[str], None] = '965a4fb49fae'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 55f24e794643
|
||||
Revises: 1fa13c2c4df4
|
||||
Create Date: 2026-03-13 23:03:47.236864
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '55f24e794643'
|
||||
down_revision: Union[str, Sequence[str], None] = '1fa13c2c4df4'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('middlename', sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'middlename')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,38 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 65b22fdefbe0
|
||||
Revises: 8925c8439e5d
|
||||
Create Date: 2026-02-23 14:28:12.782781
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '65b22fdefbe0'
|
||||
down_revision: Union[str, Sequence[str], None] = '8925c8439e5d'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('food', sa.Boolean(), nullable=True))
|
||||
op.add_column('users', sa.Column('alco', sa.Boolean(), nullable=True))
|
||||
op.add_column('users', sa.Column('types_of_alco', sa.String(), nullable=True))
|
||||
op.drop_column('users', 'checkbox1')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('checkbox1', sa.BOOLEAN(), nullable=True))
|
||||
op.drop_column('users', 'types_of_alco')
|
||||
op.drop_column('users', 'alco')
|
||||
op.drop_column('users', 'food')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 8925c8439e5d
|
||||
Revises: c842fa3d57cd
|
||||
Create Date: 2026-02-23 13:59:55.999845
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '8925c8439e5d'
|
||||
down_revision: Union[str, Sequence[str], None] = 'c842fa3d57cd'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('checkbox1', sa.Boolean(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'checkbox1')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 965a4fb49fae
|
||||
Revises: 65b22fdefbe0
|
||||
Create Date: 2026-02-28 13:42:28.964508
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '965a4fb49fae'
|
||||
down_revision: Union[str, Sequence[str], None] = '65b22fdefbe0'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,47 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: c842fa3d57cd
|
||||
Revises:
|
||||
Create Date: 2026-02-04 19:37:17.628815
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'c842fa3d57cd'
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('users',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('surname', sa.String(), nullable=True),
|
||||
sa.Column('text_field', sa.String(), nullable=True),
|
||||
sa.Column('activated', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('last_login', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('admin', sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('code')
|
||||
)
|
||||
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_users_id'), table_name='users')
|
||||
op.drop_table('users')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: dd476c0dcf61
|
||||
Revises: 1e2bd98e74a5
|
||||
Create Date: 2026-03-13 22:38:32.065614
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'dd476c0dcf61'
|
||||
down_revision: Union[str, Sequence[str], None] = '1e2bd98e74a5'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'alco')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('alco', sa.BOOLEAN(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
@@ -23,11 +23,11 @@ class User(Base):
|
||||
code = Column(String, unique=True, nullable=True)
|
||||
|
||||
name = Column(String, nullable=True)
|
||||
middlename=Column(String, nullable=True)
|
||||
surname = Column(String, nullable=True)
|
||||
text_field = Column(String, nullable=True)
|
||||
food = Column(Boolean)
|
||||
alco = Column(Boolean)
|
||||
types_of_alco = Column(String, default="Nothing")
|
||||
type_of_food = Column(String, nullable=True)
|
||||
types_of_alco = Column(String, default="Nothing", nullable=True)
|
||||
|
||||
activated = Column(Boolean)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -46,7 +46,8 @@ async def create_user(user_info):
|
||||
session.add(new_user)
|
||||
await session.commit()
|
||||
await session.refresh(new_user)
|
||||
return user
|
||||
else: return None
|
||||
return new_user
|
||||
|
||||
async def update_user(user_info):
|
||||
async with AsyncSessionLocal() as session:
|
||||
|
||||
@@ -3,7 +3,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
import server.backend.schema.pydantic as pydantic
|
||||
import server.backend.database.db as db
|
||||
from server.backend.auth.JWT import signJWT, decodeJWT
|
||||
api = FastAPI()
|
||||
api = FastAPI(openapi_url="/api/openapi.json",docs_url="/api/docs", redoc_url="/api/redoc")
|
||||
security = HTTPBearer()
|
||||
|
||||
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||
@@ -13,11 +13,14 @@ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(s
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
||||
return user
|
||||
async def check_roles(user=Depends(get_current_user)):
|
||||
if user.get("admin") != True:
|
||||
user_check = await db.list_user(user["user_id"])
|
||||
if not user_check:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
if user_check.admin != True:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
||||
return user
|
||||
|
||||
@api.post("/update", response_model=pydantic.UserUpdate)
|
||||
@api.post("/api/update", response_model=pydantic.UserUpdate)
|
||||
async def update_user(data: pydantic.UserUpdate, user=Depends(get_current_user)):
|
||||
user_check = await db.list_user(user["user_id"])
|
||||
if not user_check:
|
||||
@@ -40,17 +43,22 @@ async def update_user(data: pydantic.UserUpdate, user=Depends(get_current_user))
|
||||
updated_data = await db.update_user(updated_data)
|
||||
return updated_data
|
||||
|
||||
@api.post("/create", response_model=pydantic.UserAccess)
|
||||
@api.post("/api/create", response_model=pydantic.UserAccess)
|
||||
async def create_user(user_info: pydantic.UserCreate,user=Depends(check_roles)):
|
||||
await db.create_user(user_info)
|
||||
user = await db.create_user(user_info)
|
||||
if user == None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Code already exists for another user"
|
||||
)
|
||||
return user_info
|
||||
|
||||
@api.get("/list")
|
||||
@api.get("/api/list")
|
||||
async def list_users(user=Depends(check_roles)):
|
||||
list_of_users = await db.list_users()
|
||||
return list_of_users
|
||||
|
||||
@api.post("/auth",response_model=pydantic.Token)
|
||||
@api.post("/api/auth",response_model=pydantic.Token)
|
||||
async def auth(code:pydantic.UserAccess):
|
||||
login = await db.login_user(code)
|
||||
if login == None:
|
||||
|
||||
@@ -2,6 +2,7 @@ from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic.types import StringConstraints
|
||||
from typing_extensions import Annotated
|
||||
from typing import Optional
|
||||
import re
|
||||
|
||||
NameStr = Annotated[
|
||||
@@ -23,16 +24,18 @@ class UserOut(BaseModel):
|
||||
name: NameStr = Field(..., description="Name of the guest")
|
||||
surname: NameStr = Field(..., description="Surname of the guest")
|
||||
|
||||
class UserUpdate(UserAccess):
|
||||
name: NameStr = Field(..., description="Name of the guest")
|
||||
surname: NameStr = Field(..., description="Surname of the guest")
|
||||
text_field: str = Field("", max_length=500, description="what the guest wants")
|
||||
activated: bool = Field(False, description="activation of the guest")
|
||||
food: bool = Field(False, description="Options meat or fish")
|
||||
alco: bool = Field(False, description="if the guest will drink alco or not")
|
||||
types_of_alco: str = Field("", description="types of alco")
|
||||
class UserUpdate(BaseModel):
|
||||
code: Optional[str] = Field(None, min_length=6, max_length=6, description="Code of the guest")
|
||||
name: Optional[NameStr] = Field(None, description="Name of the guest")
|
||||
middlename: Optional[NameStr] = Field(None, description="Middlename of the guest")
|
||||
surname: Optional[NameStr] = Field(None, description="Surname of the guest")
|
||||
text_field: Optional[str] = Field(None, max_length=500, description="what the guest wants")
|
||||
activated: Optional[bool] = Field(None, description="activation of the guest")
|
||||
type_of_food: Optional[str] = Field(None, description="meat or fish")
|
||||
types_of_alco: Optional[str] = Field(None, description="types of alco")
|
||||
|
||||
class UserCreate(UserUpdate):
|
||||
code: str = Field(..., min_length=6, max_length=6, description="Code of the guest")
|
||||
admin:bool = Field(False, description="Admin privilegies")
|
||||
class Settings(BaseSettings):
|
||||
DIR:str
|
||||
|
||||
21
server/frontend/auth/login.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" href="login.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="glass-container">
|
||||
<div class="login-box">
|
||||
<h2>Login</h2>
|
||||
<form id="loginForm">
|
||||
<input type="password" id="password" name="password" required placeholder="Code">
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
74
server/frontend/auth/login.js
Normal file
@@ -0,0 +1,74 @@
|
||||
function getToken() {
|
||||
return localStorage.getItem("token") || sessionStorage.getItem("token");
|
||||
}
|
||||
function tokenCheck(){
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
window.location.href = "http://localhost:5500/server/frontend/main/index.html";
|
||||
}
|
||||
}
|
||||
document.getElementById('loginForm').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const password = document.getElementById('password').value;
|
||||
const userData = {
|
||||
password
|
||||
};
|
||||
try {
|
||||
const response = await fetch("http://localhost:8000/api/auth", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
password: userData.password
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json(); // читаем только один раз
|
||||
if (response.ok) { // сохраняем только при успехе
|
||||
// в sessionstorage до перезахода в браузер(
|
||||
sessionStorage.setItem("token", data.access_token);
|
||||
|
||||
window.location.href = 'http://localhost:5500/server/frontend/main/index.html';
|
||||
} else { //парсинг и вывод ошибок, если есть
|
||||
if (Array.isArray(data.detail)) {
|
||||
const messages = data.detail.map(e => {
|
||||
const field = e.loc.filter(locPart => locPart !== 'body').join(' -> ');
|
||||
return `${field}: ${e.msg}`;
|
||||
});
|
||||
showError(messages);
|
||||
} else if (typeof data.detail === 'string') {
|
||||
showError([data.detail]);
|
||||
} else {
|
||||
showError(['Unknown error']);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
showError(['Connection error: ' + err.message]);
|
||||
}
|
||||
});
|
||||
function showError(messages){ //Добавление их на form со стилями
|
||||
let errorElem = document.getElementById('formError');
|
||||
let container = document.getElementById('glass-container');
|
||||
if (!errorElem){
|
||||
errorElem = document.createElement('div');
|
||||
errorElem.style.transition="3s";
|
||||
errorElem.id = 'formError';
|
||||
errorElem.style.color = 'red';
|
||||
errorElem.style.marginTop = '20px';
|
||||
errorElem.style.fontSize = '14px';
|
||||
errorElem.style.fontWeight = '100';
|
||||
errorElem.style.marginBottom = '20px';
|
||||
errorElem.style.lineHeight="120%";
|
||||
errorElem.style.height = 'auto';
|
||||
const form = document.getElementById('loginForm');
|
||||
form.insertAdjacentElement('afterend', errorElem);
|
||||
};
|
||||
errorElem.innerHTML = '';
|
||||
messages.forEach(msg => {
|
||||
const li = document.createElement('li');
|
||||
li.style.listStyleType="none";
|
||||
li.textContent = msg;
|
||||
errorElem.appendChild(li);
|
||||
});
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
test
|
||||
69
server/frontend/main/api.js
Normal file
@@ -0,0 +1,69 @@
|
||||
function getToken() {
|
||||
return localStorage.getItem("token") || sessionStorage.getItem("token");
|
||||
}
|
||||
function tokenCheck(){
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
window.location.href = "http://localhost:5500/server/frontend/auth/login.html";
|
||||
}
|
||||
}
|
||||
|
||||
tokenCheck()
|
||||
document.getElementById('logoutForm').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
localStorage.removeItem("token");
|
||||
sessionStorage.removeItem("token");
|
||||
tokenCheck();
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const form = document.querySelector(".form-info");
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
// try {
|
||||
// собираем данные из формы
|
||||
const name = document.getElementById('ffname').value || "";
|
||||
const middlename = document.getElementById('fmname').value || "";
|
||||
const surname = document.getElementById('flname').value || "";
|
||||
const text_field = document.getElementById('text_field')?.value || "";
|
||||
const food = document.querySelector('input[name="food"]:checked')?.value || "";
|
||||
const types_of_alco = Array.from(document.querySelectorAll('input[name="drink"]:checked'))
|
||||
.map(el => el.value)
|
||||
.join(', ');
|
||||
|
||||
const guestData = {
|
||||
name: name,
|
||||
middlename: middlename,
|
||||
surname: surname,
|
||||
text_field: text_field,
|
||||
activated: true,
|
||||
types_of_food: food,
|
||||
types_of_alco: types_of_alco
|
||||
};
|
||||
console.log(guestData)
|
||||
|
||||
// отправка на /api/update с Authorization
|
||||
// const updateResponse = await fetch('/api/update', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'Authorization': `Bearer ${token}`
|
||||
// },
|
||||
// body: JSON.stringify(guestData)
|
||||
// });
|
||||
|
||||
// if (!updateResponse.ok) {
|
||||
// throw new Error('Ошибка при отправке формы');
|
||||
// }
|
||||
|
||||
// const updateData = await updateResponse.json();
|
||||
// console.log('Форма успешно отправлена:', updateData);
|
||||
|
||||
// } catch (err) {
|
||||
// console.error(err);
|
||||
// alert('Ошибка: ' + err.message);
|
||||
// }
|
||||
});
|
||||
});
|
||||
|
||||
1
server/frontend/main/fonts/MurreyC/COPYRIGHT.txt
Normal file
@@ -0,0 +1 @@
|
||||
DoubleRus encoding by Diai; unfinished
|
||||
BIN
server/frontend/main/fonts/MurreyC/murreyc.eot
Normal file
BIN
server/frontend/main/fonts/MurreyC/murreyc.ttf
Normal file
BIN
server/frontend/main/fonts/MurreyC/murreyc.woff
Normal file
BIN
server/frontend/main/fonts/MurreyC/murreyc.woff2
Normal file
BIN
server/frontend/main/images/1.jpg
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
server/frontend/main/images/234.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
server/frontend/main/images/236595.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
server/frontend/main/images/25.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 69 KiB |
BIN
server/frontend/main/images/55555.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
server/frontend/main/images/56532 (2).png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
server/frontend/main/images/565656565.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 171 KiB |
BIN
server/frontend/main/images/backgraund_mai копия22.jpg
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
server/frontend/main/images/backgraund_main.jpg
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
server/frontend/main/images/bc6.jpg
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
server/frontend/main/images/bg.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
server/frontend/main/images/bg1.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
server/frontend/main/images/bg2.jpg
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
server/frontend/main/images/icon.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
server/frontend/main/images/people/IMG_20260107_135956_402.jpg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
server/frontend/main/images/people/IMG_20260107_140110_500.jpg
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
server/frontend/main/images/people/IMG_20260107_140120_301.jpg
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
server/frontend/main/images/photos/IMG_20260223_151005_128.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
server/frontend/main/images/photos/IMG_20260223_151049_713.jpg
Normal file
|
After Width: | Height: | Size: 525 KiB |
11
server/frontend/main/images/telegram-svgrepo-com.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" fill="url(#paint0_linear_87_7225)"/>
|
||||
<path d="M22.9866 10.2088C23.1112 9.40332 22.3454 8.76755 21.6292 9.082L7.36482 15.3448C6.85123 15.5703 6.8888 16.3483 7.42147 16.5179L10.3631 17.4547C10.9246 17.6335 11.5325 17.541 12.0228 17.2023L18.655 12.6203C18.855 12.4821 19.073 12.7665 18.9021 12.9426L14.1281 17.8646C13.665 18.3421 13.7569 19.1512 14.314 19.5005L19.659 22.8523C20.2585 23.2282 21.0297 22.8506 21.1418 22.1261L22.9866 10.2088Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_87_7225" x1="16" y1="2" x2="16" y2="30" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#37BBFE"/>
|
||||
<stop offset="1" stop-color="#007DBB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 920 B |
BIN
server/frontend/main/images/time/1.jpg
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
server/frontend/main/images/time/2.jpg
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
server/frontend/main/images/time/3.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 2.9 MiB |
BIN
server/frontend/main/images/time/beverage_4137183.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
server/frontend/main/images/time/cake.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
server/frontend/main/images/time/camera.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
server/frontend/main/images/time/disco-ball_1034052.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
server/frontend/main/images/time/disco-ball_18181959.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
server/frontend/main/images/time/food.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
server/frontend/main/images/time/free-icon-fireworks-7201451.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
server/frontend/main/images/time/glasses.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
server/frontend/main/images/time/love-letter_6425091.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
server/frontend/main/images/time/love-world_18905915.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
server/frontend/main/images/time/message_6649165.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
server/frontend/main/images/time/ring.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
server/frontend/main/images/time/table.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
server/frontend/main/images/time/wedding-dinner_11196102.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
server/frontend/main/images/Без названия (1).jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
server/frontend/main/images/Без названия (2).jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
server/frontend/main/images/Без названия (3).jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
server/frontend/main/images/Без названия.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 97 KiB |
BIN
server/frontend/main/images/мудборд.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
529
server/frontend/main/index.html
Normal file
@@ -0,0 +1,529 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wedding invitation</title>
|
||||
<link rel="stylesheet" href="reset.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.css"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<nav class="nav">
|
||||
<ul class="menu">
|
||||
<!-- 1 -->
|
||||
<li class="item">
|
||||
<a href="#heading" class="link">
|
||||
Добро пожаловать!
|
||||
</a>
|
||||
</li>
|
||||
<!-- 2 -->
|
||||
<li class="item">
|
||||
<a href="#people" class="link">
|
||||
Список гостей
|
||||
</a>
|
||||
</li>
|
||||
<!-- 3 -->
|
||||
<li class="item">
|
||||
<a href="#timetable" class="link">
|
||||
Расписание
|
||||
</a>
|
||||
</li>
|
||||
<!-- 4 -->
|
||||
<li class="item">
|
||||
<a href="#transfer" class="link">
|
||||
Путешествие
|
||||
</a>
|
||||
</li>
|
||||
<!-- 5 -->
|
||||
<li class="item">
|
||||
<a href="#photos" class="link">
|
||||
Фотографии
|
||||
</a>
|
||||
</li>
|
||||
<!-- 6 -->
|
||||
<li class="item">
|
||||
<a href="#to-do-list" class="link">
|
||||
Список задач
|
||||
</a>
|
||||
</li>
|
||||
<!-- 7 -->
|
||||
<li class="item">
|
||||
<a href="#answer" class="link">
|
||||
Прошу ответить
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<section class="heading" id="heading">
|
||||
<article class="heading_content">
|
||||
<div class="heading_cont">
|
||||
<h1 class="heding_title">Дмитрий и Алёна</h1>
|
||||
<h2 class="heading_info">
|
||||
Наша история
|
||||
</h2>
|
||||
<h3 class="heading_date">22 августа 2026</h3>
|
||||
<p class="heading_text">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste possimus ipsa, labore repudiandae
|
||||
ratione placeat dolore vitae praesentium perspiciatis ipsam non illum reiciendis accusamus quae
|
||||
quo veritatis maiores quod sequi. Ipsam at consequatur quia recusandae, rem dicta autem quaerat
|
||||
placeat?
|
||||
</p>
|
||||
<button class="btn_share" button>
|
||||
Подтвердить участие
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<aside>
|
||||
<div class="time">
|
||||
<!-- отсчет до даты -->
|
||||
00:00:00
|
||||
</div>
|
||||
</aside>
|
||||
<section class="people" id="people">
|
||||
<h4 class="people_title">
|
||||
Список гостей
|
||||
</h4>
|
||||
<div class="newlywed">
|
||||
<div class="newlywed_item">
|
||||
<img class="img_newlywed" src="images/people//IMG_20260107_140110_500.jpg" alt="groom">
|
||||
<div class="newlywed_text">
|
||||
<h3 class="newlywed_title">Жених</h3>
|
||||
<p class="newlywed_info">Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui voluptatum
|
||||
odio autem, ut dolorum suscipit soluta reiciendis quam reprehenderit saepe doloribus
|
||||
asperiores
|
||||
architecto? Debitis magnam, exercitationem nam temporibus eos molestias?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="newlywed_item">
|
||||
<img class="img_newlywed" src="images/people/IMG_20260107_140120_301.jpg" alt="bride">
|
||||
<div class="newlywed_text">
|
||||
<h3 class="newlywed_title">Невеста</h3>
|
||||
<p class="newlywed_info">Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui voluptatum
|
||||
odio autem, ut dolorum suscipit soluta reiciendis quam reprehenderit saepe doloribus
|
||||
asperiores
|
||||
architecto? Debitis magnam, exercitationem nam temporibus eos molestias?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guest_list">
|
||||
<div class="guest_inner">
|
||||
<!-- 1 -->
|
||||
<div class="guest_item">
|
||||
<div class="guest_index">
|
||||
Стол 1
|
||||
</div>
|
||||
<div class="guest_item_list">
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 2 -->
|
||||
<div class="guest_item">
|
||||
<div class="guest_index">
|
||||
Стол 1
|
||||
</div>
|
||||
<div class="guest_item_list">
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 3 -->
|
||||
<div class="guest_item">
|
||||
<div class="guest_index">
|
||||
Стол 1
|
||||
</div>
|
||||
<div class="guest_item_list">
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
<div class="guest_name">
|
||||
Lorem, ipsum.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="timetable" id="timetable">
|
||||
<h4 class="people_title">
|
||||
Расписание
|
||||
</h4>
|
||||
<div class="time_wrapper">
|
||||
<div class="time_content">
|
||||
<div class="time_inner">
|
||||
<div class="time_row_up">
|
||||
<!-- 1 -->
|
||||
<div class="row_up_item">
|
||||
<div class="time_img_box">
|
||||
<img src="images/time/ring.png" alt="ring" class="time_img">
|
||||
<h6 class="time_h6">10:40</h6>
|
||||
<p class="time_p">Церемония</p>
|
||||
<div class="time_comment">Начало нашего свадебного дня и самый важный момент для нас
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 2 -->
|
||||
<div class="row_up_item">
|
||||
<div class="time_img_box">
|
||||
<img src="images/time/camera.png" alt="camera" class="time_img">
|
||||
<h6 class="time_h6">11:30</h6>
|
||||
<p class="time_p">Фотосессия</p>
|
||||
<div class="time_comment">После церемонии мы ненадолго отправимся на фотосессию,
|
||||
чтобы сохранить этот день в памяти</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 3 -->
|
||||
<div class="row_up_item">
|
||||
<div class="time_img_box">
|
||||
<img src="images/time/wedding-dinner_11196102.png" alt="" class="time_img">
|
||||
<h6 class="time_h6">14:00</h6>
|
||||
<p class="time_p">Фуршет и бармен</p>
|
||||
<div class="time_comment">Берем напиток, закуски и наслаждаемся началом вечера</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 4 -->
|
||||
<div class="row_up_item">
|
||||
<div class="time_img_box">
|
||||
<img src="images/time/free-icon-fireworks-7201451.png" alt="" class="time_img">
|
||||
<h6 class="time_h6">14:30</h6>
|
||||
<p class="time_p">Поздравления и подарки</p>
|
||||
<div class="time_comment">Мы будем рады теплым словам и подаркам во время фуршета
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="time_line"></div>
|
||||
<div class="time_row_down">
|
||||
<!-- 5 -->
|
||||
<div class="row_up_item">
|
||||
<div class="time_img_box">
|
||||
<img src="images/time/food.png" alt="" class="time_img">
|
||||
<h6 class="time_h6">17:00</h6>
|
||||
<p class="time_p">Банкет</p>
|
||||
<div class="time_comment">Приглашаем вас за столы, чтобы продолжить вечер в тёплой и
|
||||
уютной атмосфере. Приветственный тост</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 6 -->
|
||||
<div class="row_up_item">
|
||||
<div class="time_img_box">
|
||||
<img src="images/time/cake.png" alt="" class="time_img">
|
||||
<h6 class="time_h6">19:00</h6>
|
||||
<p class="time_p">Свадебный торт</p>
|
||||
<div class="time_comment">Самый сладкий момент вечера. Чай или кофе находятся в
|
||||
велком зоне</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 7 -->
|
||||
<div class="row_up_item">
|
||||
<div class="time_img_box">
|
||||
<img src="images/time/disco-ball_18181959.png" alt="" class="time_img">
|
||||
<h6 class="time_h6">19:30</h6>
|
||||
<p class="time_p">Танцы</p>
|
||||
<div class="time_comment">Приглашаем вас на танцпол. Танцуйте столько, сколько
|
||||
захочется</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="transfer" id="transfer">
|
||||
<h4 class="transfer_title">
|
||||
Путешествие
|
||||
</h4>
|
||||
<div class="transfer_inner">
|
||||
<div class="transfer_map">
|
||||
<div class="map">
|
||||
<div style="position:relative;overflow:hidden; border-radius: 33px;">
|
||||
<a href="https://www.google.com/maps/place/Glavnoye+Upravleniye+Zapisi+Aktov+Grazhdanskogo+Sostoyaniya+Tverskoy+Oblasti/@56.8571039,35.901453,17z/data=!3m1!4b1!4m6!3m5!1s0x46b687a4d4edacad:0x15df9a38c874eb5d!8m2!3d56.857101!4d35.9040279!16s%2Fg%2F11ls6j2lvj?entry=ttu&g_ep=EgoyMDI2MDMxMS4wIKXMDSoASAFQAw%3D%3D"
|
||||
style="color:#eee;font-size:12px;position:absolute;top:0px;">
|
||||
Главное управление записи актов гражданского состояния</a>
|
||||
<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2181.349338718406!2d35.901452977352925!3d56.85710390639812!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x46b687a4d4edacad%3A0x15df9a38c874eb5d!2sGlavnoye%20Upravleniye%20Zapisi%20Aktov%20Grazhdanskogo%20Sostoyaniya%20Tverskoy%20Oblasti!5e0!3m2!1sen!2s!4v1773428540512!5m2!1sen!2s" width="760" height="500" style="border:1px;" allowfullscreen="true" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
|
||||
</div>
|
||||
<p class="map_adress">Тверь, Свободный переулок, 5</p>
|
||||
<h6 class="map_info">Начало в</h6>
|
||||
<h6 class="map_time">10:30</h6>
|
||||
</div>
|
||||
<div class="map">
|
||||
<div style="position:relative;overflow:hidden; border-radius: 33px;">
|
||||
<a href="https://www.google.com/maps/place/Loft+1870+%7C+%D0%9F%D0%BB%D0%BE%D1%89%D0%B0%D0%B4%D0%BA%D0%B0+%D0%B4%D0%BB%D1%8F+%D0%BC%D0%B5%D1%80%D0%BE%D0%BF%D1%80%D0%B8%D1%8F%D1%82%D0%B8%D0%B9+%D0%A2%D0%B2%D0%B5%D1%80%D1%8C+%7C+%D0%BA%D0%B5%D0%B9%D1%82%D0%B5%D1%80%D0%B8%D0%BD%D0%B3+%D0%BD%D0%B0+%D1%81%D0%B2%D0%B0%D0%B4%D1%8C%D0%B1%D1%83,+%D0%B1%D0%B0%D0%BD%D0%BA%D0%B5%D1%82%D0%BD%D1%8B%D0%B9+%D0%B7%D0%B0%D0%BB/@56.850656,35.8603082,17.32z/data=!3m1!5s0x46b687a53bac0dcf:0x5c270d07d710d5a!4m6!3m5!1s0x46b68764eb0c1387:0x7cb135f5414bc1e0!8m2!3d56.8508612!4d35.8650531!16s%2Fg%2F11p0wc5v7q?entry=ttu&g_ep=EgoyMDI2MDMxMS4wIKXMDSoASAFQAw%3D%3D"
|
||||
style="color:#eee;font-size:12px;position:absolute;top:0px;">Лофт 1870</a>
|
||||
<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d1749.675516895515!2d35.86030818806184!3d56.850656040536194!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x46b68764eb0c1387%3A0x7cb135f5414bc1e0!2zTG9mdCAxODcwIHwg0J_Qu9C-0YnQsNC00LrQsCDQtNC70Y8g0LzQtdGA0L7Qv9GA0LjRj9GC0LjQuSDQotCy0LXRgNGMIHwg0LrQtdC50YLQtdGA0LjQvdCzINC90LAg0YHQstCw0LTRjNCx0YMsINCx0LDQvdC60LXRgtC90YvQuSDQt9Cw0Ls!5e0!3m2!1sen!2s!4v1773428705434!5m2!1sen!2s" width="760" height="500" style="border:1px;" allowfullscreen="true" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
|
||||
</div>
|
||||
<p class="map_adress">Тверь, улица Двор Пролетарки, 16</p>
|
||||
<h6 class="map_info">Начало в</h6>
|
||||
<h6 class="map_time">14:00</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="photos" id="photos">
|
||||
<h4 class="photos_title">
|
||||
Фотографии
|
||||
</h4>
|
||||
<!-- Slider main container -->
|
||||
<div class="swiper">
|
||||
<!-- Additional required wrapper -->
|
||||
<div class="swiper-wrapper">
|
||||
<!-- Slides -->
|
||||
<div class="swiper-slide">
|
||||
<img class="swiper_img" src="images/photos/IMG_20260223_151005_128.jpg" alt="photo">
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img class="swiper_img" src="images/photos//IMG_20260223_151049_713.jpg" alt="photo">
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img class="swiper_img" src="images/photos/IMG_20260223_151005_128.jpg" alt="photo">
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img class="swiper_img" src="images/photos/IMG_20260223_151049_713.jpg" alt="photo">
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img class="swiper_img" src="images/photos/IMG_20260223_151005_128.jpg" alt="photo">
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img class="swiper_img" src="images/photos/IMG_20260223_151049_713.jpg" alt="photo">
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img class="swiper_img" src="images/photos/IMG_20260223_151005_128.jpg" alt="photo">
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img class="swiper_img" src="images/photos/IMG_20260223_151005_128.jpg" alt="photo">
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img class="swiper_img" src="images/photos/IMG_20260223_151005_128.jpg" alt="photo">
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img class="swiper_img" src="images/photos/IMG_20260223_151005_128.jpg" alt="photo">
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img class="swiper_img" src="images/photos/IMG_20260223_151005_128.jpg" alt="photo">
|
||||
</div>
|
||||
</div>
|
||||
<!-- If we need pagination -->
|
||||
<div class="swiper-pagination"></div>
|
||||
|
||||
<!-- If we need navigation buttons -->
|
||||
<div class="swiper-button-prev"></div>
|
||||
<div class="swiper-button-next"></div>
|
||||
|
||||
<!-- If we need scrollbar -->
|
||||
<div class="swiper-scrollbar"></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="to-do-list" id="to-do-list">
|
||||
<h4 class="do-list_title">
|
||||
Список задач
|
||||
</h4>
|
||||
<div class="to-do-list_wrapper">
|
||||
<div class="to-do-list_items">
|
||||
<div class="to-do-list_item">
|
||||
<img class="to-do-list_item_img" src="images/icon.png" alt="icon">
|
||||
Поздравить молодых и сказать тёплые слова
|
||||
</div>
|
||||
<div class="to-do-list_item">
|
||||
<img class="to-do-list_item_img" src="images/icon.png" alt="icon">
|
||||
Вручить подарок молодожёнам
|
||||
</div>
|
||||
<div class="to-do-list_item">
|
||||
<img class="to-do-list_item_img" src="images/icon.png" alt="icon">
|
||||
Сделать фото и видео, чтобы потом было что выложить
|
||||
</div>
|
||||
<div class="to-do-list_item">
|
||||
<img class="to-do-list_item_img" src="images/icon.png" alt="icon">
|
||||
Поднять тост за любовь
|
||||
</div>
|
||||
<div class="to-do-list_item">
|
||||
<img class="to-do-list_item_img" src="images/icon.png" alt="icon">
|
||||
Добраться до свадебного торта, сфотографировать его
|
||||
</div>
|
||||
<div class="to-do-list_item">
|
||||
<img class="to-do-list_item_img" src="images/icon.png" alt="icon">
|
||||
Потанцевать хотя бы один медленный танец
|
||||
</div>
|
||||
<div class="to-do-list_item">
|
||||
<img class="to-do-list_item_img" src="images/icon.png" alt="icon">
|
||||
Повеселиться так, чтобы этот день запомнился
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="answer" id="answer">
|
||||
<h4 class="answer_title">Прошу ответить</h4>
|
||||
|
||||
<div class="answer_wrapper">
|
||||
<h6 class="answer_heading">
|
||||
Просим Вас заполнить форму и подтвердить своё участие
|
||||
</h6>
|
||||
|
||||
<form class="form-info">
|
||||
|
||||
<!-- Левая колонка -->
|
||||
<div class="form_name">
|
||||
<label>Имя
|
||||
<input type="text" name="firstName" id="ffname" required>
|
||||
</label>
|
||||
|
||||
<label>Отчество
|
||||
<input type="text" name="middleName" id="fmname" required>
|
||||
</label>
|
||||
|
||||
<label>Фамилия
|
||||
<input type="text" name="lastName" id="flname" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Средняя колонка -->
|
||||
<div class="form_menu">
|
||||
<p class="block_title">Горячее блюдо</p>
|
||||
|
||||
<label class="option">
|
||||
<input type="radio" name="food" id="rmeat" value="meat" required>
|
||||
Мясо
|
||||
</label>
|
||||
|
||||
<label class="option">
|
||||
<input type="radio" name="food" id="rfish" value="fish">
|
||||
Рыба
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Правая колонка -->
|
||||
<div class="form_drink">
|
||||
<p class="block_title">Напитки</p>
|
||||
|
||||
<label class="option"><input type="checkbox" name="drink" value="champagne" id="cchampagne"> Шампанское</label>
|
||||
<label class="option"><input type="checkbox" name="drink" value="wine" id="cwine"> Вино</label>
|
||||
<label class="option"><input type="checkbox" name="drink" value="vodka" id="cvodka"> Водка</label>
|
||||
<label class="option"><input type="checkbox" name="drink" value="whiskey" id="cwhiskey"> Виски</label>
|
||||
<label class="option"><input type="checkbox" name="drink" value="tequila" id="ctequila"> Текила</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="answer_btn" id="bsubmit">
|
||||
Подтвердить участие
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer_inner">
|
||||
<div class="footer_title">
|
||||
<h1 class="fotter_logo">
|
||||
Алёна и Дмитрий
|
||||
</h1>
|
||||
<p class="footer_date">22 августа 2026</p>
|
||||
<p class="footer_place">LOFT 1870</p>
|
||||
</div>
|
||||
<div class="footer_contacts">
|
||||
<p class="footer_text">"С нетерпением ждем встречи!"</p>
|
||||
<div class="footer_cntcts">
|
||||
<a class="footer_cnt" href="https://t.me/Alena7729" target="_blank"><?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="40px" height="40px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" fill="url(#paint0_linear_87_7225)"/>
|
||||
<path d="M22.9866 10.2088C23.1112 9.40332 22.3454 8.76755 21.6292 9.082L7.36482 15.3448C6.85123 15.5703 6.8888 16.3483 7.42147 16.5179L10.3631 17.4547C10.9246 17.6335 11.5325 17.541 12.0228 17.2023L18.655 12.6203C18.855 12.4821 19.073 12.7665 18.9021 12.9426L14.1281 17.8646C13.665 18.3421 13.7569 19.1512 14.314 19.5005L19.659 22.8523C20.2585 23.2282 21.0297 22.8506 21.1418 22.1261L22.9866 10.2088Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_87_7225" x1="16" y1="2" x2="16" y2="30" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#37BBFE"/>
|
||||
<stop offset="1" stop-color="#007DBB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
Алёна</a>
|
||||
<a class="footer_cnt" href="https://t.me/DisaTylov" target="_blank"><?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="40px" height="40px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" fill="url(#paint0_linear_87_7225)"/>
|
||||
<path d="M22.9866 10.2088C23.1112 9.40332 22.3454 8.76755 21.6292 9.082L7.36482 15.3448C6.85123 15.5703 6.8888 16.3483 7.42147 16.5179L10.3631 17.4547C10.9246 17.6335 11.5325 17.541 12.0228 17.2023L18.655 12.6203C18.855 12.4821 19.073 12.7665 18.9021 12.9426L14.1281 17.8646C13.665 18.3421 13.7569 19.1512 14.314 19.5005L19.659 22.8523C20.2585 23.2282 21.0297 22.8506 21.1418 22.1261L22.9866 10.2088Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_87_7225" x1="16" y1="2" x2="16" y2="30" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#37BBFE"/>
|
||||
<stop offset="1" stop-color="#007DBB"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
Дмитрий</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer_copy">
|
||||
<div class="footer_hashtag">#свадьба2026</div>
|
||||
<div class="footer_copyright">© 2026 Алёна и Дмитрий</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.js"></script>
|
||||
<script src="main.js"></script>
|
||||
<script src="api.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
30
server/frontend/main/main.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const swiper = new Swiper('.swiper', {
|
||||
// Optional parameters
|
||||
direction: 'horizontal',
|
||||
loop: true,
|
||||
|
||||
// Navigation arrows
|
||||
navigation: {
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev',
|
||||
},
|
||||
|
||||
// And if we need scrollbar
|
||||
scrollbar: {
|
||||
el: '.swiper-scrollbar',
|
||||
},
|
||||
|
||||
speed: 1000,
|
||||
breakpoints: {
|
||||
740: {
|
||||
slidesPerView: 1,
|
||||
},
|
||||
1200: {
|
||||
slidesPerView: 2,
|
||||
},
|
||||
1600: {
|
||||
slidesPerView: 3,
|
||||
},
|
||||
}
|
||||
|
||||
});
|
||||
18
server/frontend/main/reset.css
Normal file
@@ -0,0 +1,18 @@
|
||||
*{padding: 0;margin: 0;border: 0;}
|
||||
*,*:before,*:after{-moz-box-sizing: border-box;-webkit-box-sizing: border-box;box-sizing: border-box;}
|
||||
:focus,:active{outline: none;}
|
||||
a:focus,a:active{outline: none;}
|
||||
nav,footer,header,aside{display: block;}
|
||||
html,body{
|
||||
height:100%;width:100%;
|
||||
font-size:100%;line-height:1;-ms-text-size-adjust:100%;-moz-text-size-adjust:100%;-webkit-text-size-adjust:100%;}
|
||||
input,button,textarea{font-family:inherit;}
|
||||
input::-ms-clear{display: none;}
|
||||
button{cursor: pointer;}
|
||||
button::-moz-focus-inner{padding:0;border:0;}
|
||||
a,a:visited{text-decoration: none;}
|
||||
a:hover{text-decoration: none;}
|
||||
ul li{list-style: none;}
|
||||
img{vertical-align: top;}
|
||||
h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight: inherit;}
|
||||
a{color:inherit;}
|
||||
596
server/frontend/main/style.css
Normal file
@@ -0,0 +1,596 @@
|
||||
/* --------------------------------Font-------------------------------- */
|
||||
@font-face {
|
||||
font-family: "MurreyC";
|
||||
src: url("fonts/MurreyC/murreyc.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "MurreyC";
|
||||
src: url("fonts/MurreyC/murreyc.woff") format("woff");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "MurreyC";
|
||||
src: url("fonts/MurreyC/murreyc.eot") format("eot");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------------MAIN-------------------------------- */
|
||||
|
||||
body {
|
||||
background: url("images/ChatGPT\ Image\ 5\ янв.\ 2026\ г.\,\ 15_49_39.png") no-repeat 100% 100% fixed;
|
||||
background-size: cover;
|
||||
font-family: "MurreyC";
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 3px;
|
||||
height: auto;
|
||||
}
|
||||
button {
|
||||
background-color: #3a1f09;
|
||||
padding: 0 15px;
|
||||
border-radius: 33px;
|
||||
transition: all .1s;
|
||||
color: #e5b97e;
|
||||
box-shadow: #3a1f09 2px 4px 10px 2px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #f0c590;
|
||||
color: #b06029;
|
||||
transform: scale(1.015);
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
.content {
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------HEADER------------------------------ */
|
||||
.header {
|
||||
width: 100%;
|
||||
background-color: #663c24;
|
||||
padding: 20px 180px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
box-shadow: #3a1f09 2px 4px 10px 2px;
|
||||
}
|
||||
.nav {
|
||||
}
|
||||
.menu {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.item {
|
||||
color: #e7b15c;
|
||||
font-size: 36px;
|
||||
font-weight: 900;
|
||||
transition: all .3s;
|
||||
text-shadow: 1px 2px #322624;
|
||||
}
|
||||
.item:hover {
|
||||
|
||||
}
|
||||
.link {
|
||||
|
||||
transition: all .3s;
|
||||
}
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* --------------------------------Welcome----------------------------- */
|
||||
|
||||
.heading {
|
||||
background: url("images/25.png") 50% 100% / cover;
|
||||
margin: 0px 400px;
|
||||
/* border: 10px solid rgb(184, 136, 48); */
|
||||
/* border: 15px double rgb(184, 136, 48); */
|
||||
height: 640px;
|
||||
box-shadow: #000000 2px 4px 10px 2px;
|
||||
}
|
||||
.heading_content {
|
||||
padding: 15px 100px;
|
||||
}
|
||||
.heading_cont {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 20px 0px 20px 0px;
|
||||
text-align: center;
|
||||
box-shadow: #000000 2px 4px 10px 2px;
|
||||
}
|
||||
.heading_cont::after {
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
content: "";
|
||||
display: block;
|
||||
background-color: rgba(184, 136, 48, 0.8);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
border: 1px solid #42240c;
|
||||
}
|
||||
.heding_title {
|
||||
font-size: 42px;
|
||||
font-weight: 900;
|
||||
/* text-decoration: underline; */
|
||||
}
|
||||
.heading_info {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
/* color: #eccfae; */
|
||||
}
|
||||
.heading_date {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
border-top: 3px solid #000;
|
||||
border-bottom: 3px solid #000;
|
||||
/* background-image: linear-gradient(to left, rgb(66, 36, 12), rgb(184, 136, 48)); */
|
||||
}
|
||||
.heading_text {
|
||||
font-size: 26px;
|
||||
padding: 0 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn_share {
|
||||
margin-top: 10px;
|
||||
font-size: 30px;
|
||||
}
|
||||
.asise {
|
||||
}
|
||||
.time{
|
||||
padding-top: 15px;
|
||||
font-size: 110px;
|
||||
text-align: center;
|
||||
background-image: linear-gradient(90deg,rgba(26, 21, 26, 1) 0%, rgba(101, 62, 39, 1) 50%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
text-shadow: #000 2px 2px 6px;
|
||||
}
|
||||
|
||||
/* -----------------------------------PEOPLE-------------------------------------- */
|
||||
|
||||
.people {
|
||||
width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 80px;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.heading_h4 {
|
||||
margin-top: 40px;
|
||||
}
|
||||
h4 {
|
||||
display: block;
|
||||
border-top: 3px solid #000;
|
||||
border-bottom: 3px solid #000 ;
|
||||
font-size: 60px;
|
||||
text-align: center;
|
||||
font-weight: 900;
|
||||
width: 30%;
|
||||
margin: 0 auto;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.newlywed {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 40px;
|
||||
width: 100%;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.newlywed_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: rgba(245, 198, 124, 0.9);
|
||||
padding: 20px;
|
||||
box-shadow: #000000 2px 4px 10px 2px;
|
||||
}
|
||||
.img_newlywed {
|
||||
width: 40%;
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #47312c;
|
||||
}
|
||||
.newlywed_text {
|
||||
margin-left: 20px;
|
||||
width: 60%;
|
||||
}
|
||||
.newlywed_title {
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
text-decoration:underline;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.newlywed_info {
|
||||
font-size: 26px;
|
||||
}
|
||||
.guest_list {
|
||||
}
|
||||
.guest_inner {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.guest_item {
|
||||
text-align: center;
|
||||
background: url("images/55555.jpg") 100% 100% / cover;
|
||||
padding: 40px 80px;
|
||||
border-radius: 33px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.guest_index {
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
color: rgb(252, 220, 180);
|
||||
}
|
||||
.guest_item_list {
|
||||
|
||||
}
|
||||
.guest_name {
|
||||
font-size: 36px;
|
||||
font-style: italic;
|
||||
color: #e5b97e;
|
||||
}
|
||||
|
||||
/* ---------------------------------TIMETABLE----------------------------------- */
|
||||
.timetable {
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
padding-bottom: 60px;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.time_wrapper {
|
||||
height: 700px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
.time_wrapper::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.time_inner {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
.time_content {
|
||||
z-index: 10;
|
||||
}
|
||||
.time_row_up {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.row_up_item {
|
||||
background-color: #e5b97e;
|
||||
width: 320px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
border-radius: 33px;
|
||||
margin: 0 20px;
|
||||
}
|
||||
.time_img_box {
|
||||
|
||||
}
|
||||
.time_img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.time_line {
|
||||
}
|
||||
.time_row_down {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.time_h6 {
|
||||
font-size: 26px;
|
||||
font-weight: 900;
|
||||
}
|
||||
.time_p {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
text-decoration: underline;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.time_comment {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* ----------------------------------TRANSFER--------------------------------------- */
|
||||
|
||||
.transfer {
|
||||
margin-bottom: 60px;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.transfer_title {
|
||||
}
|
||||
.transfer_inner {
|
||||
margin-top: 60px;
|
||||
}
|
||||
.transfer_map {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 100px;
|
||||
}
|
||||
.map > div {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
.map_adress {
|
||||
font-size: 50px;
|
||||
text-align: center;
|
||||
text-decoration: underline;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.map_info {
|
||||
font-size: 50px;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.map_time {
|
||||
font-size: 50px;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/* --------------------------------PHOTOS-------------------------------------- */
|
||||
.photos {
|
||||
padding-bottom: 140px;
|
||||
margin: 0 auto;
|
||||
padding-top: 60px;
|
||||
}
|
||||
.photos_title {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
.swiper_img {
|
||||
height: 500px;
|
||||
border: 5px dotted #000;
|
||||
}
|
||||
.swiper {
|
||||
width: 95%;
|
||||
height: 550px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.swiper-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.swiper-slide {
|
||||
text-align: center;
|
||||
}
|
||||
.swiper-navigation-icon {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* --------------------------------------------TODOLIST------------------------------------------------------ */
|
||||
|
||||
.to-do-list {
|
||||
padding-bottom: 100px;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.do-list_title {
|
||||
}
|
||||
.to-do-list_wrapper {
|
||||
width: 100%;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.to-do-list_items {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
width: 60%;
|
||||
}
|
||||
.to-do-list_item {
|
||||
background: linear-gradient(135deg, #f3d9c6, #e8c3a4, #d9a77a);
|
||||
backdrop-filter: blur(6px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(0,0,0,0.05);
|
||||
color: #3e2a1f;
|
||||
text-shadow: 0 1px 1px rgba(255,255,255,0.3);
|
||||
}
|
||||
.to-do-list_item_img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
/* ---------------------------ANSWER----------------------------------- */
|
||||
|
||||
.answer {
|
||||
padding-bottom: 100px;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.answer_wrapper {
|
||||
background: linear-gradient(135deg, #f3d9c6, #e8c3a4, #d9a77a);
|
||||
padding: 60px;
|
||||
margin: 0 400px;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 15px 40px rgba(0,0,0,0.08);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.answer_heading {
|
||||
text-align: center;
|
||||
font-size: 42px;
|
||||
margin-bottom: 50px;
|
||||
color: #2f1e14;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.form-info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 50px;
|
||||
}
|
||||
|
||||
.form_name {
|
||||
color: #2f1e14;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.block_title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 20px;
|
||||
color: #2f1e14;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
padding: 12px 15px;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background: #fffaf3;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.answer_btn {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 40px;
|
||||
padding: 18px 40px;
|
||||
border-radius: 30px;
|
||||
border: none;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
background: #2f1e14;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: 0.3s ease;
|
||||
}
|
||||
|
||||
.answer_btn:hover {
|
||||
background: #1e140d;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.form_name input[type="text"] {
|
||||
width: 320px; /* одинаковая ширина */
|
||||
height: 50px; /* одинаковая высота */
|
||||
padding: 0 20px;
|
||||
border-radius: 25px;
|
||||
border: none;
|
||||
background-color: #ffffff;
|
||||
outline: none;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
/* -----------------------------FOOTER---------------------------------- */
|
||||
|
||||
.footer {
|
||||
background-color: #2f1e14;
|
||||
color: #fff;
|
||||
box-shadow: 0 -4px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.footer_inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
.footer_title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.fotter_logo {
|
||||
font-size: 42px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.footer_date {
|
||||
font-size: 36px;
|
||||
}
|
||||
.footer_place {
|
||||
font-size: 30px;
|
||||
}
|
||||
.footer_contacts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.footer_cnt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 30px;
|
||||
transition: all .3s;
|
||||
}
|
||||
.footer_cnt:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.footer_cnt svg:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.footer_text {
|
||||
font-size: 50px;
|
||||
}
|
||||
.footer_cntcts {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
.footer_copy {
|
||||
font-size: 30px;
|
||||
text-align: end;
|
||||
}
|
||||
.footer_hashtag {
|
||||
text-decoration: underline;
|
||||
font-size: 36px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.footer_copyright {
|
||||
}
|
||||