Função de trocar a fonte

This commit is contained in:
Murilo 2025-07-07 02:10:56 -03:00
parent 89f051b7f3
commit 37842d2879
11 changed files with 414 additions and 57 deletions

141
alembic.ini Normal file
View file

@ -0,0 +1,141 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

36
app.py
View file

@ -219,6 +219,7 @@ class Party(db.Model):
location = db.Column(db.String(255), nullable=True)
event_date = db.Column(db.Date, nullable=True)
event_time = db.Column(db.Time, nullable=True)
invite_font = db.Column(db.String(100), nullable=False, default='Montserrat-Regular')
@property
def formatted_date(self):
@ -304,14 +305,19 @@ def generate_qr_code_image(qr_data, guest_name, party, output_format='PNG'):
GUEST_NAME_COLOR = (37, 99, 235)
try:
font_path_regular = FONT_PATH
# Use a fonte selecionada para a festa, com fallback para Montserrat-Regular
selected_font_name = party.invite_font if party.invite_font else 'Montserrat-Regular'
font_path_regular = os.path.join(basedir, 'static', 'fonts', f"{selected_font_name}.ttf")
# Tenta carregar a versão Bold se existir, caso contrário usa a regular
font_path_bold = font_path_regular.replace("Regular", "Bold")
if not os.path.exists(font_path_bold): font_path_bold = font_path_regular
font_party_name = PILImageFont.truetype(font_path_bold, 52)
font_guest_name = PILImageFont.truetype(font_path_bold, 36)
font_footer = PILImageFont.truetype(font_path_regular, 14)
font_footer = PILImageFont.truetype(os.path.join(basedir, "Montserrat-Regular.ttf"), 14) # Footer always Montserrat
except IOError:
app.logger.warning(f"Fonte personalizada não encontrada em {FONT_PATH}. Usando fonte padrão.")
app.logger.warning(f"Fonte personalizada {selected_font_name} não encontrada. Usando fonte padrão.")
font_party_name, font_guest_name, font_footer = (PILImageFont.load_default(s) for s in [52,36,14])
logo_img_raw, logo_height = None, 0
@ -1013,6 +1019,30 @@ def get_party_stats_data(party_id):
def get_stats(party_id):
return jsonify(get_party_stats_data(party_id))
@app.route('/api/party/<int:party_id>/font_selection', methods=['GET', 'POST'])
@login_required
def handle_font_selection(party_id):
party = db.session.get(Party, party_id) or abort(404)
check_collaboration_permission(party)
if request.method == 'GET':
return jsonify({'selected_font': party.invite_font})
elif request.method == 'POST':
data = request.get_json()
font_name = data.get('font_name')
if not font_name:
return jsonify({'message': 'Nome da fonte não fornecido.'}), 400
# Basic validation to ensure the font is one of the allowed ones
allowed_fonts = ['Birthstone-Regular', 'Ephesis-Regular', 'Montserrat-Regular', 'Radley-Regular']
if font_name not in allowed_fonts:
return jsonify({'message': 'Fonte selecionada inválida.'}), 400
party.invite_font = font_name
db.session.commit()
return jsonify({'message': 'Fonte do convite atualizada com sucesso!', 'selected_font': party.invite_font})
@app.route('/api/party/<int:party_id>/guests', methods=['GET', 'POST'])
@login_required
def handle_guests(party_id):

View file

@ -1,9 +1,14 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
import os
import sys
# Adicione o diretório raiz do projeto ao sys.path para importar app
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from app import db, User, Party, Guest # Importe db e seus modelos
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
@ -14,41 +19,26 @@ config = context.config
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
# this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db
target_metadata = db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
# Configura a URL do banco de dados diretamente da variável de ambiente
DATABASE_URL = os.environ.get('DATABASE_URL')
if not DATABASE_URL:
raise ValueError("A variável de ambiente 'DATABASE_URL' não foi definida. Ela é essencial para a conexão com o PostgreSQL.")
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
# Algumas plataformas (como Heroku e versões mais antigas do Render) usam "postgres://"
# enquanto o SQLAlchemy espera "postgresql://". Esta linha garante a compatibilidade.
if DATABASE_URL.startswith("postgres://"):
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://", 1)
config.set_main_option('sqlalchemy.url', DATABASE_URL)
def run_migrations_offline():
@ -65,7 +55,10 @@ def run_migrations_offline():
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
@ -79,35 +72,38 @@ def run_migrations_online():
and associate a connection with the context.
"""
from sqlalchemy import engine_from_config
from sqlalchemy import pool
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
conf_args = current_app.extensions['migrate'].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
connectable = get_engine()
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
target_metadata=target_metadata,
compare_type=True, # Importante para detectar mudanças de tipo
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
process_revision_directives=process_revision_directives,
)
with context.begin_transaction():
context.run_migrations()
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
run_migrations_online()

View file

@ -0,0 +1,37 @@
"""Add invite_font to Party model
Revision ID: 491a3e67287b
Revises: ab5f97567bec
Create Date: 2025-07-07 01:51:28.636888
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '491a3e67287b'
down_revision = 'ab5f97567bec'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('party', schema=None) as batch_op:
batch_op.add_column(sa.Column('invite_font', sa.String(length=100), nullable=True))
op.execute("UPDATE party SET invite_font = 'Montserrat-Regular' WHERE invite_font IS NULL")
with op.batch_alter_table('party', schema=None) as batch_op:
batch_op.alter_column('invite_font',
existing_type=sa.String(length=100),
nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('party', schema=None) as batch_op:
batch_op.drop_column('invite_font')
# ### end Alembic commands ###

View file

@ -1,8 +1,8 @@
"""Reset e criação inicial das tabelas
Revision ID: 6f810791ec7d
Revision ID: ab5f97567bec
Revises:
Create Date: 2025-07-02 01:12:00.632624
Create Date: 2025-07-07 01:21:08.654559
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6f810791ec7d'
revision = 'ab5f97567bec'
down_revision = None
branch_labels = None
depends_on = None

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
This is a placeholder for Montserrat-Regular.ttf. Please replace this with the actual font file content.

Binary file not shown.

View file

@ -16,7 +16,46 @@
{% block content %}
<style>
<style>
@font-face {
font-family: 'Birthstone-Regular';
src: url('{{ url_for('static', filename='fonts/Birthstone-Regular.ttf') }}') format('truetype');
}
@font-face {
font-family: 'Ephesis-Regular';
src: url('{{ url_for('static', filename='fonts/Ephesis-Regular.ttf') }}') format('truetype');
}
@font-face {
font-family: 'Montserrat-Regular';
src: url('{{ url_for('static', filename='fonts/Montserrat-Regular.ttf') }}') format('truetype');
}
@font-face {
font-family: 'Radley-Regular';
src: url('{{ url_for('static', filename='fonts/Radley-Regular.ttf') }}') format('truetype');
}
.font-button {
padding: 1rem;
border-radius: 0.75rem;
background-color: var(--tw-bg-opacity);
--tw-bg-opacity: 0.1;
box-shadow: var(--tw-shadow-inner);
text-align: center;
font-size: 1.25rem;
font-weight: 600;
color: var(--tw-text-opacity);
--tw-text-opacity: 1;
transition: all 0.2s;
cursor: pointer;
border: 2px solid transparent;
}
.font-button:hover {
--tw-bg-opacity: 0.2;
box-shadow: var(--tw-shadow-lg);
}
.font-button.selected {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-primary-light), var(--tw-shadow-lg);
}
.modal, .toast-container { transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out; }
.modal-content, .toast { transition: transform 0.3s ease-in-out; }
.modal.show .modal-content, .toast.show { transform: scale(1); }
@ -60,7 +99,11 @@
<button class="tab-button flex-1 py-3 text-center text-lg font-semibold text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary transition-colors duration-200 border-b-2 border-transparent data-[active=true]:border-primary data-[active=true]:text-primary dark:data-[active=true]:text-primary" data-tab="add-invite">
<i class="fas fa-user-plus mr-2"></i>Adicionar & Convidar
</button>
<!-- ABA 5: Configurações -->
<!-- ABA 5: Customizar Convite -->
<button class="tab-button flex-1 py-3 text-center text-lg font-semibold text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary transition-colors duration-200 border-b-2 border-transparent data-[active=true]:border-primary data-[active=true]:text-primary dark:data-[active=true]:text-primary" data-tab="customize-invite">
<i class="fas fa-paint-brush mr-2"></i>Customizar Convite
</button>
<!-- ABA 6: Configurações -->
<button class="tab-button flex-1 py-3 text-center text-lg font-semibold text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary transition-colors duration-200 border-b-2 border-transparent data-[active=true]:border-primary data-[active=true]:text-primary dark:data-[active=true]:text-primary" data-tab="settings">
<i class="fas fa-cog mr-2"></i>Configurações
</button>
@ -201,7 +244,19 @@
</div>
</div>
<!-- ABA 5: CONTEÚDO DAS CONFIGURAÇÕES -->
<!-- ABA 5: CONTEÚDO DE CUSTOMIZAR CONVITE -->
<div id="tab-customize-invite" class="tab-content pt-6 hidden fade-in">
<div class="glass-card rounded-2xl shadow-inner p-6">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center"><i class="fas fa-paint-brush text-primary mr-3"></i>Customizar Convite</h2>
<p class="text-gray-600 dark:text-gray-300 mb-4">Escolha a fonte para o nome da sua festa no convite:</p>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4" id="font-selection-container">
<!-- Font buttons will be loaded here by JavaScript -->
</div>
<button id="saveFontButton" class="mt-6 w-full bg-gradient-to-r from-primary to-secondary text-white py-3 rounded-xl font-semibold hover:shadow-lg transition-all duration-200 hover-scale">Salvar Fonte Escolhida</button>
</div>
</div>
<!-- ABA 6: CONTEÚDO DAS CONFIGURAÇÕES -->
<div id="tab-settings" class="tab-content pt-6 hidden fade-in">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Coluna Principal (2/3) -->
@ -550,6 +605,8 @@
fetchStats();
} else if (tabId === 'checkin-analytics' && !isCheckinChartInitialized) {
initCheckinAnalyticsChart();
} else if (tabId === 'customize-invite') {
loadFontsAndRenderButtons();
}
}
@ -881,6 +938,82 @@
attendanceChartInstance = new Chart(ctx, config);
}
}
// --- Lógica para seleção de fonte ---
const availableFonts = [
{ name: 'Birthstone-Regular', display: 'Birthstone Regular' },
{ name: 'Ephesis-Regular', display: 'Ephesis Regular' },
{ name: 'Montserrat-Regular', display: 'Montserrat Regular' },
{ name: 'Radley-Regular', display: 'Radley Regular' }
];
let selectedFont = 'Montserrat-Regular'; // Default font
async function loadFontsAndRenderButtons() {
const fontContainer = document.getElementById('font-selection-container');
fontContainer.innerHTML = '<div class="col-span-full text-center py-4"><i class="fas fa-spinner fa-spin text-2xl text-primary"></i> Carregando fontes...</div>';
try {
// Fetch current selected font for the party
const response = await fetch(`${API_URL}/font_selection`);
if (response.ok) {
const data = await response.json();
if (data.selected_font) {
selectedFont = data.selected_font;
}
}
fontContainer.innerHTML = ''; // Clear loading message
availableFonts.forEach(font => {
const button = document.createElement('button');
button.className = `font-button glass-effect text-primary dark:text-white ${selectedFont === font.name ? 'selected' : ''}`;
button.style.fontFamily = `'${font.name}', sans-serif`;
button.textContent = font.display;
button.dataset.fontName = font.name;
button.onclick = () => selectFont(font.name);
fontContainer.appendChild(button);
});
document.getElementById('saveFontButton').onclick = saveSelectedFont;
} catch (error) {
console.error('Error loading fonts or fetching selected font:', error);
fontContainer.innerHTML = '<div class="col-span-full text-center py-4 text-danger">Erro ao carregar fontes.</div>';
showToast('Erro ao carregar fontes.', 'error');
}
}
function selectFont(fontName) {
selectedFont = fontName;
document.querySelectorAll('.font-button').forEach(button => {
if (button.dataset.fontName === fontName) {
button.classList.add('selected');
} else {
button.classList.remove('selected');
}
});
}
async function saveSelectedFont() {
try {
const response = await fetch(`${API_URL}/font_selection`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ font_name: selectedFont })
});
const data = await response.json();
if (response.ok) {
showToast(data.message, 'success');
} else {
throw new Error(data.message || 'Erro ao salvar a fonte.');
}
} catch (error) {
console.error('Error saving font:', error);
showToast(error.message, 'error');
}
}
</script>
{% endblock %}

View file

@ -20,6 +20,25 @@
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
<style>
@font-face {
font-family: 'Birthstone-Regular';
src: url('{{ url_for('static', filename='fonts/Birthstone-Regular.ttf') }}') format('truetype');
}
@font-face {
font-family: 'Ephesis-Regular';
src: url('{{ url_for('static', filename='fonts/Ephesis-Regular.ttf') }}') format('truetype');
}
@font-face {
font-family: 'Montserrat-Regular';
src: url('{{ url_for('static', filename='fonts/Montserrat-Regular.ttf') }}') format('truetype');
}
@font-face {
font-family: 'Radley-Regular';
src: url('{{ url_for('static', filename='fonts/Radley-Regular.ttf') }}') format('truetype');
}
</style>
<script>
tailwind.config = {
darkMode: 'class',
@ -78,7 +97,7 @@
{% endif %}
<div class="p-6 sm:p-8">
<h1 class="text-4xl font-extrabold text-gray-900 dark:text-white tracking-tight text-center">{{ party.name }}</h1>
<h1 class="text-4xl font-extrabold text-gray-900 dark:text-white tracking-tight text-center" style="font-family: '{{ party.invite_font if party.invite_font else 'Montserrat-Regular' }}', sans-serif;">{{ party.name }}</h1>
<div class="mt-8 space-y-5 text-left border-t border-gray-200 dark:border-gray-700/50 pt-6">