init
This commit is contained in:
31
app.py
Executable file
31
app.py
Executable file
@ -0,0 +1,31 @@
|
||||
from flask import Flask, render_template, request
|
||||
import math
|
||||
from profiles import profiles
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def euclidean_distance(a, b):
|
||||
return math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b)))
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def index():
|
||||
if request.method == "POST":
|
||||
user_values = [int(request.form.get(f"point{i}", 2)) for i in range(1, 7)]
|
||||
|
||||
# Find 3 best matches sorted by distance ascending
|
||||
sorted_matches = sorted(profiles.items(), key=lambda p: euclidean_distance(user_values, p[1]['values']))
|
||||
top_matches = sorted_matches[:3] # top 3 matches
|
||||
|
||||
return render_template(
|
||||
"result.html",
|
||||
user=user_values,
|
||||
top_matches=top_matches,
|
||||
)
|
||||
return render_template("quiz.html")
|
||||
|
||||
@app.route("/profiles")
|
||||
def profiles_page():
|
||||
return render_template("profiles.html", profiles=profiles)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='0.0.0.0', port=1337, debug=True)
|
BIN
static/ccl.png
Executable file
BIN
static/ccl.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
static/favicon.png
Executable file
BIN
static/favicon.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
319
static/style.css
Normal file
319
static/style.css
Normal file
@ -0,0 +1,319 @@
|
||||
/* === Reset & Base Styles === */
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-family: Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.75rem;
|
||||
font-weight: bold;
|
||||
margin: 0 0 0.5em 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: bold;
|
||||
margin: 0 0 0.75em 0;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 1em 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input[type="hidden"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* === Layout Containers === */
|
||||
body.profiles-page {
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(69deg, #39ff14, #00ffcc, #adff2f);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientMove 39s ease infinite;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.grid {
|
||||
max-width: 2000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 15px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* === Cards === */
|
||||
.profile-card {
|
||||
background: rgba(255, 255, 255, 0.39);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 90%;
|
||||
max-width: 320px;
|
||||
min-height: 320px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.profile-card canvas {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
.radar-container {
|
||||
background: rgba(255, 255, 255, 0.39);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.radar-container h2,
|
||||
.radar-container h3 {
|
||||
margin: 8px 0 10px 0;
|
||||
}
|
||||
|
||||
.radar-container canvas {
|
||||
width: 100% !important;
|
||||
max-width: 320px;
|
||||
height: 320px !important;
|
||||
max-height: 320px;
|
||||
object-fit: contain;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.radar-container p.info-text,
|
||||
.profile-card p.info-text {
|
||||
text-align: center;
|
||||
margin: 6px 10px 0;
|
||||
font-size: 1rem;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
/* === Category Cards === */
|
||||
.cards-container {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
padding: 20px;
|
||||
width: 90%;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.category-card.active {
|
||||
border-color: #00aaff;
|
||||
background: rgba(0, 170, 255, 0.2);
|
||||
}
|
||||
|
||||
.category-card .checkmark {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
font-size: 18px;
|
||||
color: green;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.category-card.active .checkmark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* === Matches === */
|
||||
.matches-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.matches-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.best-card {
|
||||
border: 2px solid gold;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.others-row {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.others-row .profile-card {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* === Buttons === */
|
||||
button[type="submit"],
|
||||
button.magenta-button {
|
||||
display: block;
|
||||
margin: 30px 0;
|
||||
padding: 15px 40px;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
background-color: #00aaff;
|
||||
}
|
||||
|
||||
button[type="submit"]:hover {
|
||||
background-color: #0088cc;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
button[type="submit"]:active {
|
||||
background-color: #006699;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button.magenta-button {
|
||||
background-color: #ff00aa;
|
||||
}
|
||||
|
||||
button.magenta-button:hover {
|
||||
background-color: #cc0088;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
button.magenta-button:active {
|
||||
background-color: #990066;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
margin: 30px auto;
|
||||
min-height: 5%;
|
||||
width: 100%;
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
|
||||
.button-row button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* === Animations === */
|
||||
@keyframes gradientMove {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* === Media Queries === */
|
||||
@media (max-width: 600px) {
|
||||
.radar-container {
|
||||
width: 90vw !important;
|
||||
max-width: none !important;
|
||||
padding-bottom: 10px;
|
||||
height: auto;
|
||||
}
|
||||
.radar-container canvas {
|
||||
width: 90vw !important;
|
||||
max-width: none;
|
||||
height: 90vw !important;
|
||||
max-height: none;
|
||||
}
|
||||
.logo {
|
||||
max-width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
body.profiles-page {
|
||||
background-size: 400% 1600%;
|
||||
}
|
||||
.cards-container {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 899px) and (min-width: 600px) {
|
||||
body.profiles-page {
|
||||
background-size: 400% 1600%;
|
||||
}
|
||||
.cards-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
body.profiles-page {
|
||||
background-size: 400% 1600%;
|
||||
}
|
||||
.cards-container {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
82
templates/profiles.html
Executable file
82
templates/profiles.html
Executable file
@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>canculator profiles</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}">
|
||||
</head>
|
||||
<body class="profiles-page">
|
||||
<div class="logo-container">
|
||||
<a href="{{ url_for('index') }}">
|
||||
<img src="{{ url_for('static', filename='ccl.png') }}" alt="Logo" class="logo">
|
||||
</a>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button type="button" class="magenta-button" onclick="location.href='{{ url_for('index') }}'">New Quiz</button>
|
||||
</div>
|
||||
<div class="grid">
|
||||
{% for name, values in profiles.items() %}
|
||||
<div class="profile-card">
|
||||
<div class="profile-name"><h2>{{ name }}</h2></div>
|
||||
<canvas id="chart-{{ loop.index }}"></canvas>
|
||||
<p class="info-text">{{ values.info_text1 }}</p>
|
||||
<p class="info-text">{{ values.info_text2 }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const labels = ['Energy', 'Calm', 'Relax', 'Sleep', 'Focus', 'Inspire'];
|
||||
const profiles = {{ profiles | tojson }};
|
||||
|
||||
Object.entries(profiles).forEach(([name, data], index) => {
|
||||
const ctx = document.getElementById(`chart-${index + 1}`).getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'radar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: name,
|
||||
data: data.values,
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(0, 150, 255, 0.24)', // electric blue fill
|
||||
borderColor: 'rgb(0, 150, 255)', // electric blue border
|
||||
pointBackgroundColor: 'rgb(0, 150, 255)'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
r: {
|
||||
min: 0,
|
||||
max: 80,
|
||||
ticks: {
|
||||
stepSize: 20,
|
||||
display: false
|
||||
},
|
||||
pointLabels: {
|
||||
font: { size: 14 }
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(255,255,255,0.69)',
|
||||
circular: true
|
||||
},
|
||||
angleLines: {
|
||||
color: 'rgba(255,255,255,0.69)',
|
||||
circular: true
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
57
templates/quiz.html
Executable file
57
templates/quiz.html
Executable file
@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>canculator quiz</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}">
|
||||
</head>
|
||||
<body class="profiles-page">
|
||||
<div class="logo-container">
|
||||
<a href="{{ url_for('index') }}">
|
||||
<img src="{{ url_for('static', filename='ccl.png') }}" alt="Logo" class="logo">
|
||||
</a>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button type="button" class="magenta-button" onclick="location.href='{{ url_for('profiles_page') }}'">All Profiles</button>
|
||||
</div>
|
||||
<h2>What are you looking for?</h2>
|
||||
<form method="POST">
|
||||
<div class="cards-container">
|
||||
{% set categories = [
|
||||
{'name': 'Energy', 'desc': 'Boost endurance and alertness.'},
|
||||
{'name': 'Calm', 'desc': 'Stay grounded and centered.'},
|
||||
{'name': 'Relax', 'desc': 'Ease tension in body and mind.'},
|
||||
{'name': 'Sleep', 'desc': 'Support restfulness and recovery.'},
|
||||
{'name': 'Focus', 'desc': 'Enhance concentration and clarity.'},
|
||||
{'name': 'Inspire', 'desc': 'Spark creativity and new ideas.'}
|
||||
] %}
|
||||
{% for cat in categories %}
|
||||
<div class="category-card" onclick="toggleCard(this, {{ loop.index }})">
|
||||
<h3>{{ cat.name }}</h3>
|
||||
<p>{{ cat.desc }}</p>
|
||||
<input type="hidden" name="point{{ loop.index }}" id="point{{ loop.index }}" value="30">
|
||||
<div class="checkmark">✓</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button type="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function toggleCard(card, index) {
|
||||
const input = document.getElementById(`point${index}`);
|
||||
if (card.classList.contains('active')) {
|
||||
card.classList.remove('active');
|
||||
input.value = "30"; // inactive score
|
||||
} else {
|
||||
card.classList.add('active');
|
||||
input.value = "69"; // active score
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
152
templates/result.html
Executable file
152
templates/result.html
Executable file
@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>canculator result</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" />
|
||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}" />
|
||||
</head>
|
||||
<body class="profiles-page">
|
||||
<div class="logo-container">
|
||||
<a href="{{ url_for('index') }}">
|
||||
<img src="{{ url_for('static', filename='ccl.png') }}" alt="Logo" class="logo" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button type="button" class="magenta-button" onclick="location.href='{{ url_for('profiles_page') }}'">All Profiles</button>
|
||||
<button type="button" class="magenta-button" onclick="location.href='{{ url_for('index') }}'">New Quiz</button>
|
||||
</div>
|
||||
<h2>Your Matches:</h2>
|
||||
<div class="matches-container">
|
||||
<div class="matches-row best-row">
|
||||
{% set match_labels = ["Best", "Runner-up", "Maybe..."] %}
|
||||
{% if top_matches|length > 0 %}
|
||||
{% set name, match_values = top_matches[0] %}
|
||||
<div class="profile-card best-card">
|
||||
<h3>{{ match_labels[0] }}</h3>
|
||||
<h2>{{ name }}</h2>
|
||||
<canvas id="radarChart-1"></canvas>
|
||||
<p class="info-text">{{ match_values.info_text1 }}</p>
|
||||
<p class="info-text">{{ match_values.info_text2 }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="matches-row others-row">
|
||||
{% for idx in range(1, top_matches|length) %}
|
||||
{% set name, match_values = top_matches[idx] %}
|
||||
<div class="profile-card">
|
||||
<h3>{{ match_labels[idx] if idx < match_labels|length else 'Match' }}</h3>
|
||||
<h2>{{ name }}</h2>
|
||||
<canvas id="radarChart-{{ idx + 1 }}"></canvas>
|
||||
<p class="info-text">{{ match_values.info_text1 }}</p>
|
||||
<p class="info-text">{{ match_values.info_text2 }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const labels = ['Energy', 'Calm', 'Relax', 'Sleep', 'Focus', 'Inspire'];
|
||||
const userData = {{ user | tojson }};
|
||||
const matches = {{ top_matches | tojson }};
|
||||
|
||||
matches.forEach(([name, profile], index) => {
|
||||
const ctx = document.getElementById(`radarChart-${index + 1}`).getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'radar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'You',
|
||||
data: userData,
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(255, 20, 147, 0.24)',
|
||||
borderColor: 'rgb(255, 20, 147)',
|
||||
pointBackgroundColor: 'rgb(255, 20, 147)',
|
||||
},
|
||||
{
|
||||
label: name,
|
||||
data: profile.values,
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(0, 150, 255, 0.24)',
|
||||
borderColor: 'rgb(0, 150, 255)',
|
||||
pointBackgroundColor: 'rgb(0, 150, 255)',
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
r: {
|
||||
min: 0,
|
||||
max: 80,
|
||||
ticks: {
|
||||
stepSize: 20,
|
||||
display: false
|
||||
},
|
||||
pointLabels: {
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(255,255,255,0.69)',
|
||||
circular: true
|
||||
},
|
||||
angleLines: {
|
||||
color: 'rgba(255,255,255,0.69)',
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top',
|
||||
labels: {
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
window.addEventListener('load', () => {
|
||||
const bestCard = document.querySelector('.best-card');
|
||||
if (!bestCard) return;
|
||||
|
||||
const rect = bestCard.getBoundingClientRect();
|
||||
const colors = ['#FF00AA', '#FFD700']; // magenta and gold
|
||||
|
||||
function fire(xOrigin) {
|
||||
confetti({
|
||||
particleCount: 69,
|
||||
angle: xOrigin < 0.5 ? 60 : 120,
|
||||
spread: 69,
|
||||
origin: {
|
||||
x: xOrigin,
|
||||
y: (rect.top + rect.height / 2) / window.innerHeight
|
||||
},
|
||||
colors: colors
|
||||
});
|
||||
}
|
||||
|
||||
// Fire once from left and right edges of the best-card
|
||||
const leftOrigin = (rect.left) / window.innerWidth;
|
||||
const rightOrigin = (rect.right) / window.innerWidth;
|
||||
|
||||
fire(leftOrigin);
|
||||
fire(rightOrigin);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user