giseldo commited on
Commit
c96b1d5
·
1 Parent(s): 7b5385d
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ __pycahche__
Dockerfile DELETED
@@ -1,17 +0,0 @@
1
- # Usa uma imagem Python como base
2
- FROM python:3.9
3
-
4
- # Define o diretório de trabalho dentro do contêiner
5
- WORKDIR /app
6
-
7
- # Copia os arquivos do projeto para o contêiner
8
- COPY . /app
9
-
10
- # Instala as dependências
11
- RUN pip install -r requirements.txt
12
-
13
- # Expõe a porta do Flask
14
- EXPOSE 5000
15
-
16
- # Comando para rodar o app
17
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md DELETED
@@ -1,10 +0,0 @@
1
- ---
2
- title: Eventos V2
3
- emoji: 🔥
4
- colorFrom: red
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -129,7 +129,7 @@ def login():
129
  'access_token': response.session.access_token
130
  }
131
 
132
- return redirect(url_for('dashboard'))
133
 
134
  except Exception as e:
135
  return render_template('login.html', error=str(e))
@@ -137,9 +137,9 @@ def login():
137
  return render_template('login.html')
138
 
139
  # Rota para o painel do usuário (protegida)
140
- @app.route('/dashboard')
141
  @login_required
142
- def dashboard():
143
  user = session.get('user')
144
  # Verificar se o usuário é admin
145
  try:
@@ -150,7 +150,7 @@ def dashboard():
150
 
151
  is_admin = True
152
 
153
- return render_template('dashboard.html', user=user, is_admin=is_admin)
154
 
155
  # Rota para logout
156
  @app.route('/logout')
@@ -165,14 +165,14 @@ def logout():
165
 
166
  # Rota para listar conferências (admin)
167
  @app.route('/admin/conferences')
168
- @admin_required
169
  def admin_conferences():
170
  conferences = get_conferences_from_db()
171
  return render_template('admin/conferences.html', conferences=conferences)
172
 
173
  # Rota para adicionar nova conferência
174
  @app.route('/admin/conferences/new', methods=['GET', 'POST'])
175
- @admin_required
176
  def add_conference():
177
  print("entrou aqui")
178
  if request.method == 'POST':
@@ -183,7 +183,6 @@ def add_conference():
183
  'full_name': request.form.get('full_name'),
184
  'dates': request.form.get('dates'),
185
  'location': request.form.get('location'),
186
- 'categories': request.form.get('categories').split(','),
187
  'deadline': request.form.get('deadline'),
188
  'website': request.form.get('website'),
189
  'description': request.form.get('description'),
@@ -205,7 +204,7 @@ def add_conference():
205
 
206
  # Rota para editar conferência
207
  @app.route('/admin/conferences/edit/<conference_id>', methods=['GET', 'POST'])
208
- @admin_required
209
  def edit_conference(conference_id):
210
  # Obter a conferência pelo ID
211
  conference = get_conference_by_id(conference_id)
@@ -222,7 +221,6 @@ def edit_conference(conference_id):
222
  'full_name': request.form.get('full_name'),
223
  'dates': request.form.get('dates'),
224
  'location': request.form.get('location'),
225
- 'categories': request.form.get('categories').split(','),
226
  'deadline': request.form.get('deadline'),
227
  'website': request.form.get('website'),
228
  'description': request.form.get('description'),
@@ -243,7 +241,7 @@ def edit_conference(conference_id):
243
 
244
  # Rota para excluir conferência
245
  @app.route('/admin/conferences/delete/<conference_id>', methods=['POST'])
246
- @admin_required
247
  def delete_conference(conference_id):
248
  try:
249
  # Excluir do Supabase
@@ -272,5 +270,4 @@ def perfil():
272
  return render_template('perfil.html', user=user)
273
 
274
  if __name__ == '__main__':
275
- app.run(host="0.0.0.0", port=7860)
276
- #app.run(debug=True)
 
129
  'access_token': response.session.access_token
130
  }
131
 
132
+ return redirect(url_for('configuracao'))
133
 
134
  except Exception as e:
135
  return render_template('login.html', error=str(e))
 
137
  return render_template('login.html')
138
 
139
  # Rota para o painel do usuário (protegida)
140
+ @app.route('/configuracao')
141
  @login_required
142
+ def configuracao():
143
  user = session.get('user')
144
  # Verificar se o usuário é admin
145
  try:
 
150
 
151
  is_admin = True
152
 
153
+ return render_template('configuracao.html', user=user, is_admin=is_admin)
154
 
155
  # Rota para logout
156
  @app.route('/logout')
 
165
 
166
  # Rota para listar conferências (admin)
167
  @app.route('/admin/conferences')
168
+ #@admin_required
169
  def admin_conferences():
170
  conferences = get_conferences_from_db()
171
  return render_template('admin/conferences.html', conferences=conferences)
172
 
173
  # Rota para adicionar nova conferência
174
  @app.route('/admin/conferences/new', methods=['GET', 'POST'])
175
+ #@admin_required
176
  def add_conference():
177
  print("entrou aqui")
178
  if request.method == 'POST':
 
183
  'full_name': request.form.get('full_name'),
184
  'dates': request.form.get('dates'),
185
  'location': request.form.get('location'),
 
186
  'deadline': request.form.get('deadline'),
187
  'website': request.form.get('website'),
188
  'description': request.form.get('description'),
 
204
 
205
  # Rota para editar conferência
206
  @app.route('/admin/conferences/edit/<conference_id>', methods=['GET', 'POST'])
207
+ #@admin_required
208
  def edit_conference(conference_id):
209
  # Obter a conferência pelo ID
210
  conference = get_conference_by_id(conference_id)
 
221
  'full_name': request.form.get('full_name'),
222
  'dates': request.form.get('dates'),
223
  'location': request.form.get('location'),
 
224
  'deadline': request.form.get('deadline'),
225
  'website': request.form.get('website'),
226
  'description': request.form.get('description'),
 
241
 
242
  # Rota para excluir conferência
243
  @app.route('/admin/conferences/delete/<conference_id>', methods=['POST'])
244
+ #@admin_required
245
  def delete_conference(conference_id):
246
  try:
247
  # Excluir do Supabase
 
270
  return render_template('perfil.html', user=user)
271
 
272
  if __name__ == '__main__':
273
+ app.run(debug=True)
 
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
  flask==2.3.3
2
  supabase==2.0.3
3
  python-dotenv==1.0.0
 
4
  uuid==1.30
 
1
  flask==2.3.3
2
  supabase==2.0.3
3
  python-dotenv==1.0.0
4
+ gunicorn==21.2.0
5
  uuid==1.30
templates/admin/conference_form.html CHANGED
@@ -12,7 +12,7 @@
12
  <div class="container mt-4">
13
  <nav aria-label="breadcrumb">
14
  <ol class="breadcrumb">
15
- <li class="breadcrumb-item"><a href="{{ url_for('dashboard') }}">Dashboard</a></li>
16
  <li class="breadcrumb-item"><a href="{{ url_for('admin_conferences') }}">Conferências</a></li>
17
  <li class="breadcrumb-item active" aria-current="page">
18
  {% if action == 'add' %}Nova Conferência{% else %}Editar Conferência{% endif %}
@@ -65,7 +65,7 @@
65
 
66
  <div class="row mb-3">
67
  <div class="col-md-6">
68
- <label for="deadline" class="form-label">Prazo*</label>
69
  <input type="text" class="form-control" id="deadline" name="deadline"
70
  value="{{ conference.deadline if conference else '' }}" required
71
  placeholder="YYYY-MM-DD HH:MM">
@@ -75,14 +75,6 @@
75
  </div>
76
  </div>
77
 
78
- <div class="mb-3">
79
- <label for="categories" class="form-label">Categorias*</label>
80
- <input type="text" class="form-control" id="categories" name="categories"
81
- value="{% if conference %}{{ conference.categories|join(',') }}{% endif %}" required
82
- placeholder="machine-learning, natural-language-processing">
83
- <div class="form-text">Separadas por vírgula, sem espaços.</div>
84
- </div>
85
-
86
  <div class="mb-3">
87
  <label for="website" class="form-label">Website*</label>
88
  <input type="text" class="form-control" id="website" name="website"
 
12
  <div class="container mt-4">
13
  <nav aria-label="breadcrumb">
14
  <ol class="breadcrumb">
15
+ <li class="breadcrumb-item"><a href="{{ url_for('configuracao') }}">Configuração</a></li>
16
  <li class="breadcrumb-item"><a href="{{ url_for('admin_conferences') }}">Conferências</a></li>
17
  <li class="breadcrumb-item active" aria-current="page">
18
  {% if action == 'add' %}Nova Conferência{% else %}Editar Conferência{% endif %}
 
65
 
66
  <div class="row mb-3">
67
  <div class="col-md-6">
68
+ <label for="deadline" class="form-label">Prazo da submissão*</label>
69
  <input type="text" class="form-control" id="deadline" name="deadline"
70
  value="{{ conference.deadline if conference else '' }}" required
71
  placeholder="YYYY-MM-DD HH:MM">
 
75
  </div>
76
  </div>
77
 
 
 
 
 
 
 
 
 
78
  <div class="mb-3">
79
  <label for="website" class="form-label">Website*</label>
80
  <input type="text" class="form-control" id="website" name="website"
templates/admin/conferences.html CHANGED
@@ -93,7 +93,7 @@
93
  </div>
94
 
95
  <div class="mt-3">
96
- <a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Voltar para Dashboard</a>
97
  </div>
98
  </div>
99
  {% endblock %}
 
93
  </div>
94
 
95
  <div class="mt-3">
96
+ <a href="{{ url_for('configuracao') }}" class="btn btn-secondary">Voltar para configuração</a>
97
  </div>
98
  </div>
99
  {% endblock %}
templates/{dashboard.html → configuracao.html} RENAMED
@@ -1,30 +1,18 @@
1
  {% extends "layout.html" %}
2
 
3
- {% block title %}Dashboard - Flask Supabase Auth{% endblock %}
4
 
5
  {% block content %}
6
  <div class="row">
7
  <div class="col-md-12">
8
  <div class="card">
9
  <div class="card-header">
10
- <h2>Dashboard</h2>
11
  </div>
12
  <div class="card-body">
13
  <h5 class="card-title">Bem-vindo, {{ user.email }}!</h5>
14
  <p class="card-text">Você está autenticado com sucesso!</p>
15
 
16
- <div class="mt-4">
17
- <h6>Suas informações:</h6>
18
- <ul class="list-group">
19
- <li class="list-group-item">
20
- <strong>ID:</strong> {{ user.id }}
21
- </li>
22
- <li class="list-group-item">
23
- <strong>Email:</strong> {{ user.email }}
24
- </li>
25
- </ul>
26
- </div>
27
-
28
  {% if is_admin %}
29
  <div class="mt-4">
30
  <h6>Recursos de Administrador:</h6>
 
1
  {% extends "layout.html" %}
2
 
3
+ {% block title %}configuracao - Flask Supabase Auth{% endblock %}
4
 
5
  {% block content %}
6
  <div class="row">
7
  <div class="col-md-12">
8
  <div class="card">
9
  <div class="card-header">
10
+ <h2>Configuração</h2>
11
  </div>
12
  <div class="card-body">
13
  <h5 class="card-title">Bem-vindo, {{ user.email }}!</h5>
14
  <p class="card-text">Você está autenticado com sucesso!</p>
15
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  {% if is_admin %}
17
  <div class="mt-4">
18
  <h6>Recursos de Administrador:</h6>
templates/index.html CHANGED
@@ -3,49 +3,45 @@
3
  {% block title %}Home - Flask Supabase Auth{% endblock %}
4
 
5
  {% block content %}
6
- <div class="p-5 mb-4 bg-light rounded-3">
7
- <div class="container-fluid py-5 text-center">
8
- <h1 class="display-5 fw-bold">AI Conferences deadline</h1>
9
- <p class="fs-4">Seu calendário completo de eventos em Ciência da Computação.</p>
10
- {% if not session.get('user') %}
11
- <div class="mt-4">
12
- <a href="{{ url_for('signup') }}" class="btn btn-primary btn-lg mx-2">Registrar</a>
13
- <a href="{{ url_for('login') }}" class="btn btn-outline-primary btn-lg mx-2">Entrar</a>
14
- </div>
15
- {% else %}
16
- <div class="mt-4">
17
- <a href="{{ url_for('dashboard') }}" class="btn btn-success btn-lg">Acessar Dashboard</a>
18
- </div>
19
- {% endif %}
20
- </div>
21
- </div>
22
 
23
  <!-- Seção de Eventos/Conferências -->
24
  <div class="container mt-5">
25
- <h2 class="mb-4 text-center">Conferências de IA Disponíveis</h2>
26
- <div class="table-responsive">
27
- <table class="table table-hover">
 
 
 
 
 
 
 
 
 
 
 
 
28
  <thead class="table-dark">
29
  <tr>
30
- <th>Nome</th>
31
- <th>Datas</th>
32
- <th>Localização</th>
33
- <th>Prazo</th>
34
- <th>Tempo Restante</th>
35
- <th>Ações</th>
36
  </tr>
37
  </thead>
38
  <tbody>
39
  {% for conference in conferences %}
40
  <tr>
41
- <td>{{ conference.name }} - {{ conference.full_name }}</td>
42
- <td>{{ conference.dates }}</td>
43
- <td>{{ conference.location }}</td>
44
- <td>{{ conference.deadline }}</td>
45
- <td class="countdown-cell" data-deadline="{{ conference.deadline }}">
46
  <span class="countdown-value">Calculando...</span>
47
  </td>
48
- <td>
49
  <a href="{{ url_for('conference_details', conference_id=conference.id) }}"
50
  class="btn btn-primary btn-sm">Ver Detalhes</a>
51
  </td>
@@ -54,17 +50,83 @@
54
  </tbody>
55
  </table>
56
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  </div>
58
 
59
  <script>
60
  document.addEventListener('DOMContentLoaded', function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  // Atualizar os contadores de tempo restante
62
  function updateCountdowns() {
63
- const countdownCells = document.querySelectorAll('.countdown-cell');
 
 
 
 
 
 
 
64
 
65
- countdownCells.forEach(cell => {
66
- const deadlineStr = cell.getAttribute('data-deadline');
67
- const countdownElement = cell.querySelector('.countdown-value');
 
 
 
 
 
68
 
69
  // Verificar se a data é válida
70
  if (!deadlineStr) {
@@ -87,7 +149,7 @@
87
 
88
  if (timeDiff <= 0) {
89
  countdownElement.textContent = 'Prazo encerrado';
90
- cell.classList.add('text-danger');
91
  } else {
92
  // Calcular dias, horas, minutos restantes
93
  const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
@@ -103,9 +165,9 @@
103
 
104
  // Adicionar classes para destacar prazos próximos
105
  if (days < 1) {
106
- cell.classList.add('text-danger', 'fw-bold');
107
  } else if (days < 7) {
108
- cell.classList.add('text-warning');
109
  }
110
  }
111
  } catch (error) {
@@ -118,6 +180,110 @@
118
  // Executar imediatamente e depois a cada minuto
119
  updateCountdowns();
120
  setInterval(updateCountdowns, 60000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  });
122
  </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  {% endblock %}
 
3
  {% block title %}Home - Flask Supabase Auth{% endblock %}
4
 
5
  {% block content %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  <!-- Seção de Eventos/Conferências -->
8
  <div class="container mt-5">
9
+ <div class="d-flex justify-content-between align-items-center mb-4">
10
+ <h2 class="mb-0">Eventos Disponíveis</h2>
11
+ <div class="btn-group" role="group" aria-label="Opções de visualização">
12
+ <button type="button" class="btn btn-outline-primary view-toggle active" data-view="table">
13
+ <i class="bi bi-table"></i> Tabela
14
+ </button>
15
+ <button type="button" class="btn btn-outline-primary view-toggle" data-view="cards">
16
+ <i class="bi bi-grid-3x3-gap"></i> Cards
17
+ </button>
18
+ </div>
19
+ </div>
20
+
21
+ <!-- Visualização em Tabela -->
22
+ <div class="table-responsive view-content" id="tableView">
23
+ <table class="table table-hover" id="conferencesTable">
24
  <thead class="table-dark">
25
  <tr>
26
+ <th class="sortable text-start align-middle bg-primary" data-sort="name">Nome<i class="sort-icon"></i></th>
27
+ <th class="sortable text-start align-middle bg-primary" data-sort="dates">Datas realização<i class="sort-icon"></i></th>
28
+ <th class="sortable text-start align-middle bg-primary" data-sort="location">Localização<i class="sort-icon"></i></th>
29
+ <th class="sortable text-start align-middle bg-primary" data-sort="deadline">Prazo submissão<i class="sort-icon"></i></th>
30
+ <th class="sortable text-start align-middle bg-primary" data-sort="countdown">Tempo restante para submissão<i class="sort-icon"></i></th>
31
+ <th class="text-start align-middle bg-primary">Ações</th>
32
  </tr>
33
  </thead>
34
  <tbody>
35
  {% for conference in conferences %}
36
  <tr>
37
+ <td data-value="{{ conference.name }}" class="align-middle">{{ conference.name }} - {{ conference.full_name }}</td>
38
+ <td data-value="{{ conference.dates }}" class="align-middle">{{ conference.dates }}</td>
39
+ <td data-value="{{ conference.location }}" class="align-middle">{{ conference.location }}</td>
40
+ <td data-value="{{ conference.deadline }}" class="align-middle">{{ conference.deadline }}</td>
41
+ <td class="countdown-cell align-middle" data-deadline="{{ conference.deadline }}" data-value="{{ conference.deadline }}">
42
  <span class="countdown-value">Calculando...</span>
43
  </td>
44
+ <td class="align-middle">
45
  <a href="{{ url_for('conference_details', conference_id=conference.id) }}"
46
  class="btn btn-primary btn-sm">Ver Detalhes</a>
47
  </td>
 
50
  </tbody>
51
  </table>
52
  </div>
53
+
54
+ <!-- Visualização em Cards -->
55
+ <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 view-content" id="cardsView" style="display: none;">
56
+ {% for conference in conferences %}
57
+ <div class="col">
58
+ <div class="card h-100 shadow-sm">
59
+ <div class="card-header bg-primary text-white">
60
+ <h5 class="card-title mb-0">{{ conference.name }}</h5>
61
+ </div>
62
+ <div class="card-body">
63
+ <h6 class="card-subtitle mb-2 text-muted">{{ conference.full_name }}</h6>
64
+ <p class="card-text"><strong>Datas:</strong> {{ conference.dates }}</p>
65
+ <p class="card-text"><strong>Localização:</strong> {{ conference.location }}</p>
66
+ <p class="card-text"><strong>Prazo Submissão:</strong> {{ conference.deadline }}</p>
67
+ <p class="card-text">
68
+ <strong>Tempo Restante:</strong><br>
69
+ <span class="countdown-value card-countdown" data-deadline="{{ conference.deadline }}">Calculando...</span>
70
+ </p>
71
+ </div>
72
+ <div class="card-footer bg-transparent border-top-0">
73
+ <a href="{{ url_for('conference_details', conference_id=conference.id) }}"
74
+ class="btn btn-primary btn-sm w-100">Ver Detalhes</a>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ {% endfor %}
79
+ </div>
80
  </div>
81
 
82
  <script>
83
  document.addEventListener('DOMContentLoaded', function() {
84
+ // Configuração de alternância de visualização
85
+ const viewToggles = document.querySelectorAll('.view-toggle');
86
+ const tableView = document.getElementById('tableView');
87
+ const cardsView = document.getElementById('cardsView');
88
+
89
+ viewToggles.forEach(toggle => {
90
+ toggle.addEventListener('click', function() {
91
+ // Remover classe active de todos os botões
92
+ viewToggles.forEach(btn => btn.classList.remove('active'));
93
+ // Adicionar classe active ao botão clicado
94
+ this.classList.add('active');
95
+
96
+ // Alternar visualização com base no botão clicado
97
+ const view = this.getAttribute('data-view');
98
+ if (view === 'table') {
99
+ tableView.style.display = 'block';
100
+ cardsView.style.display = 'none';
101
+ } else {
102
+ tableView.style.display = 'none';
103
+ cardsView.style.display = 'flex';
104
+ }
105
+
106
+ // Atualizar os contadores
107
+ updateCountdowns();
108
+ });
109
+ });
110
+
111
  // Atualizar os contadores de tempo restante
112
  function updateCountdowns() {
113
+ // Atualizar para células na tabela
114
+ updateCountdownElements('.countdown-cell .countdown-value');
115
+ // Atualizar para cards
116
+ updateCountdownElements('.card-countdown');
117
+ }
118
+
119
+ function updateCountdownElements(selector) {
120
+ const countdownElements = document.querySelectorAll(selector);
121
 
122
+ countdownElements.forEach(countdownElement => {
123
+ // Pegar o deadline do elemento pai (célula da tabela) ou do próprio elemento (card)
124
+ let deadlineStr;
125
+ if (countdownElement.classList.contains('card-countdown')) {
126
+ deadlineStr = countdownElement.getAttribute('data-deadline');
127
+ } else {
128
+ deadlineStr = countdownElement.closest('.countdown-cell').getAttribute('data-deadline');
129
+ }
130
 
131
  // Verificar se a data é válida
132
  if (!deadlineStr) {
 
149
 
150
  if (timeDiff <= 0) {
151
  countdownElement.textContent = 'Prazo encerrado';
152
+ countdownElement.classList.add('text-danger');
153
  } else {
154
  // Calcular dias, horas, minutos restantes
155
  const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
 
165
 
166
  // Adicionar classes para destacar prazos próximos
167
  if (days < 1) {
168
+ countdownElement.classList.add('text-danger', 'fw-bold');
169
  } else if (days < 7) {
170
+ countdownElement.classList.add('text-warning');
171
  }
172
  }
173
  } catch (error) {
 
180
  // Executar imediatamente e depois a cada minuto
181
  updateCountdowns();
182
  setInterval(updateCountdowns, 60000);
183
+
184
+ // Configuração da ordenação da tabela
185
+ const table = document.getElementById('conferencesTable');
186
+ const headers = table.querySelectorAll('th.sortable');
187
+ let currentSort = { column: null, direction: 'asc' };
188
+
189
+ // Adicionar eventos de clique aos cabeçalhos
190
+ headers.forEach(header => {
191
+ header.addEventListener('click', () => {
192
+ const column = header.dataset.sort;
193
+ const direction = column === currentSort.column && currentSort.direction === 'asc' ? 'desc' : 'asc';
194
+
195
+ // Atualizar ícones
196
+ headers.forEach(h => h.querySelector('.sort-icon').textContent = '');
197
+ header.querySelector('.sort-icon').textContent = direction === 'asc' ? ' ▲' : ' ▼';
198
+
199
+ // Ordenar tabela
200
+ sortTable(column, direction);
201
+
202
+ // Atualizar estado de ordenação atual
203
+ currentSort = { column, direction };
204
+ });
205
+ });
206
+
207
+ function sortTable(column, direction) {
208
+ const rows = Array.from(table.querySelectorAll('tbody tr'));
209
+
210
+ // Ordenar linhas
211
+ const sortedRows = rows.sort((a, b) => {
212
+ const cellA = a.querySelector(`td[data-value]:nth-child(${getColumnIndex(column) + 1})`);
213
+ const cellB = b.querySelector(`td[data-value]:nth-child(${getColumnIndex(column) + 1})`);
214
+
215
+ let valueA = cellA ? cellA.getAttribute('data-value') : '';
216
+ let valueB = cellB ? cellB.getAttribute('data-value') : '';
217
+
218
+ // Ordenação para datas
219
+ if (column === 'deadline' || column === 'dates') {
220
+ valueA = valueA ? new Date(valueA).getTime() : 0;
221
+ valueB = valueB ? new Date(valueB).getTime() : 0;
222
+ }
223
+
224
+ // Ordenação para strings
225
+ if (typeof valueA === 'string' && typeof valueB === 'string') {
226
+ return direction === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
227
+ }
228
+
229
+ // Ordenação para números
230
+ return direction === 'asc' ? valueA - valueB : valueB - valueA;
231
+ });
232
+
233
+ // Limpar e recriar a tabela
234
+ const tbody = table.querySelector('tbody');
235
+ while (tbody.firstChild) {
236
+ tbody.removeChild(tbody.firstChild);
237
+ }
238
+
239
+ sortedRows.forEach(row => {
240
+ tbody.appendChild(row);
241
+ });
242
+ }
243
+
244
+ function getColumnIndex(columnName) {
245
+ let index = 0;
246
+ headers.forEach((header, i) => {
247
+ if (header.dataset.sort === columnName) {
248
+ index = i;
249
+ }
250
+ });
251
+ return index;
252
+ }
253
  });
254
  </script>
255
+
256
+ <style>
257
+ .sortable {
258
+ cursor: pointer;
259
+ user-select: none;
260
+ }
261
+
262
+ .sortable:hover {
263
+ background-color: #2c3034;
264
+ }
265
+
266
+ .sort-icon {
267
+ display: inline-block;
268
+ width: 10px;
269
+ }
270
+
271
+ .view-toggle.active {
272
+ background-color: #0d6efd;
273
+ color: white;
274
+ }
275
+
276
+ #cardsView {
277
+ display: flex;
278
+ flex-wrap: wrap;
279
+ }
280
+
281
+ .card {
282
+ transition: transform 0.2s;
283
+ }
284
+
285
+ .card:hover {
286
+ transform: translateY(-5px);
287
+ }
288
+ </style>
289
  {% endblock %}
templates/layout.html CHANGED
@@ -3,24 +3,37 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>{% block title %}AI Conference Deadlines{% endblock %}</title>
7
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
  <style>
10
  body {
11
  padding-top: 5rem;
 
 
 
12
  }
13
  .form-container {
14
  max-width: 500px;
15
  margin: 0 auto;
16
  padding: 1rem;
17
  }
 
 
 
 
 
 
 
 
 
 
18
  </style>
19
  </head>
20
  <body>
21
  <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
22
  <div class="container">
23
- <a class="navbar-brand" href="/">AI Conference Deadlines</a>
24
  <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
25
  <span class="navbar-toggler-icon"></span>
26
  </button>
@@ -28,7 +41,7 @@
28
  <ul class="navbar-nav ms-auto">
29
  {% if session.get('user') %}
30
  <li class="nav-item">
31
- <a class="nav-link" href="{{ url_for('dashboard') }}">Dashboard</a>
32
  </li>
33
  <li class="nav-item">
34
  <a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
@@ -38,7 +51,7 @@
38
  <a class="nav-link" href="{{ url_for('login') }}">Login</a>
39
  </li>
40
  <li class="nav-item">
41
- <a class="nav-link" href="{{ url_for('signup') }}">Signup</a>
42
  </li>
43
  {% endif %}
44
  </ul>
@@ -50,6 +63,12 @@
50
  {% block content %}{% endblock %}
51
  </main>
52
 
 
 
 
 
 
 
53
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
54
  </body>
55
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}Calendário de Eventos{% endblock %}</title>
7
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
  <style>
10
  body {
11
  padding-top: 5rem;
12
+ min-height: 100vh;
13
+ display: flex;
14
+ flex-direction: column;
15
  }
16
  .form-container {
17
  max-width: 500px;
18
  margin: 0 auto;
19
  padding: 1rem;
20
  }
21
+ main {
22
+ flex: 1 0 auto;
23
+ }
24
+ footer {
25
+ margin-top: 2rem;
26
+ padding: 1rem 0;
27
+ background-color: #f8f9fa;
28
+ text-align: center;
29
+ border-top: 1px solid #e9ecef;
30
+ }
31
  </style>
32
  </head>
33
  <body>
34
  <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
35
  <div class="container">
36
+ <a class="navbar-brand" href="/">Calendário de Eventos</a>
37
  <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
38
  <span class="navbar-toggler-icon"></span>
39
  </button>
 
41
  <ul class="navbar-nav ms-auto">
42
  {% if session.get('user') %}
43
  <li class="nav-item">
44
+ <a class="nav-link" href="{{ url_for('configuracao') }}">Configuração</a>
45
  </li>
46
  <li class="nav-item">
47
  <a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
 
51
  <a class="nav-link" href="{{ url_for('login') }}">Login</a>
52
  </li>
53
  <li class="nav-item">
54
+ <a class="nav-link" href="{{ url_for('signup') }}">Criar conta</a>
55
  </li>
56
  {% endif %}
57
  </ul>
 
63
  {% block content %}{% endblock %}
64
  </main>
65
 
66
+ <footer class="footer">
67
+ <div class="container">
68
+ <p class="text-muted">Criado por Giseldo Neo 2025</p>
69
+ </div>
70
+ </footer>
71
+
72
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
73
  </body>
74
  </html>
test_app.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ from unittest.mock import patch, MagicMock
3
+ from flask import Flask
4
+ import json
5
+ from app import app
6
+
7
+ class TestAddConference(unittest.TestCase):
8
+
9
+ def setUp(self):
10
+ self.app = app
11
+ self.app.config['TESTING'] = True
12
+ self.client = self.app.test_client()
13
+ # Criar sessão fictícia para simular usuário logado
14
+ with self.client.session_transaction() as sess:
15
+ sess['user'] = {
16
+ 'id': 'test-user-id',
17
+ 'email': '[email protected]',
18
+ 'access_token': 'fake-token'
19
+ }
20
+
21
+ @patch('app.supabase')
22
+ def test_add_conference_success(self, mock_supabase):
23
+ # Configurar o mock do Supabase para simular sucesso na inserção
24
+ mock_response = MagicMock()
25
+ mock_response.data = [{"id": "test123"}]
26
+ mock_supabase.table.return_value.insert.return_value.execute.return_value = mock_response
27
+
28
+ # Dados de conferência para teste
29
+ conference_data = {
30
+ 'id': 'test123',
31
+ 'name': 'ICML',
32
+ 'full_name': 'International Conference on Machine Learning',
33
+ 'dates': '10-15 July 2024',
34
+ 'location': 'Vienna, Austria',
35
+ 'categories': 'machine-learning, artificial-intelligence',
36
+ 'deadline': '2024-01-30 23:59',
37
+ 'website': 'icml2024.org',
38
+ 'description': 'A leading international academic conference in machine learning.'
39
+ }
40
+
41
+ # Fazer a requisição POST para adicionar uma conferência
42
+ response = self.client.post('/admin/conferences/new',
43
+ data=conference_data,
44
+ follow_redirects=True)
45
+
46
+ # Verificar se o status da resposta é 200 OK
47
+ self.assertEqual(response.status_code, 200)
48
+
49
+ # Verificar se o Supabase foi chamado corretamente
50
+ mock_supabase.table.assert_called_once_with('conferences')
51
+ mock_supabase.table().insert.assert_called_once()
52
+
53
+ # Verificar se a resposta contém a mensagem de sucesso
54
+ self.assertIn(b'Confer\xc3\xaancia adicionada com sucesso!', response.data)
55
+
56
+ @patch('app.supabase')
57
+ def test_add_conference_error(self, mock_supabase):
58
+ # Configurar o mock para simular um erro
59
+ mock_supabase.table.return_value.insert.return_value.execute.side_effect = Exception('Erro de teste')
60
+
61
+ # Dados incompletos para provocar erro
62
+ conference_data = {
63
+ 'name': 'ICML',
64
+ 'full_name': 'International Conference on Machine Learning',
65
+ # outros campos omitidos propositalmente
66
+ }
67
+
68
+ # Fazer a requisição POST
69
+ response = self.client.post('/admin/conferences/new',
70
+ data=conference_data,
71
+ follow_redirects=True)
72
+
73
+ # Verificar se a resposta contém a mensagem de erro
74
+ self.assertIn(b'Erro ao adicionar confer\xc3\xaancia: Erro de teste', response.data)
75
+
76
+ @patch('app.supabase')
77
+ def test_add_conference_with_auto_id(self, mock_supabase):
78
+ # Configurar o mock do Supabase
79
+ mock_response = MagicMock()
80
+ mock_response.data = [{"id": "auto123"}]
81
+ mock_supabase.table.return_value.insert.return_value.execute.return_value = mock_response
82
+
83
+ # Dados sem ID (deve ser gerado automaticamente)
84
+ conference_data = {
85
+ 'name': 'NeurIPS',
86
+ 'full_name': 'Neural Information Processing Systems',
87
+ 'dates': '9-15 December 2024',
88
+ 'location': 'Vancouver, Canada',
89
+ 'categories': 'neural-networks,deep-learning',
90
+ 'deadline': '2024-05-17 23:59',
91
+ 'website': 'neurips2024.cc',
92
+ 'description': 'Leading conference on neural networks and deep learning'
93
+ }
94
+
95
+ # Fazer a requisição POST
96
+ response = self.client.post('/admin/conferences/new',
97
+ data=conference_data,
98
+ follow_redirects=True)
99
+
100
+ # Verificar se o status da resposta é 200 OK
101
+ self.assertEqual(response.status_code, 200)
102
+
103
+ # Verificar que o método para inserir no Supabase foi chamado
104
+ mock_supabase.table.assert_called_once_with('conferences')
105
+ mock_supabase.table().insert.assert_called_once()
106
+
107
+ # Verificamos que um ID foi gerado (não podemos checar o valor exato pois é aleatório)
108
+ call_args = mock_supabase.table().insert.call_args[0][0]
109
+ self.assertIsNotNone(call_args.get('id'))
110
+
111
+ if __name__ == '__main__':
112
+ unittest.main()