Enhance website's visual appeal and mobile responsiveness with style updates

Update CSS styles across various components to improve visual presentation and ensure better responsiveness on mobile devices, including adjustments to spacing, aspect ratios, and element sizing.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: c446bc9e-66df-438c-a86c-f53e6da13649
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
This commit is contained in:
pac7
2025-09-23 22:11:05 +00:00
parent 4c954fff6f
commit d978217577
8 changed files with 897 additions and 231 deletions

View File

@@ -55,7 +55,7 @@ localPort = 5000
externalPort = 80
[[ports]]
localPort = 40077
localPort = 33323
externalPort = 3002
[[ports]]

View File

@@ -410,9 +410,15 @@
.top-0 {
top: calc(var(--spacing) * 0);
}
.top-1\.5 {
top: calc(var(--spacing) * 1.5);
}
.top-1\/2 {
top: calc(1/2 * 100%);
}
.top-2 {
top: calc(var(--spacing) * 2);
}
.top-3 {
top: calc(var(--spacing) * 3);
}
@@ -425,6 +431,12 @@
.right-0 {
right: calc(var(--spacing) * 0);
}
.right-1\.5 {
right: calc(var(--spacing) * 1.5);
}
.right-2 {
right: calc(var(--spacing) * 2);
}
.right-3 {
right: calc(var(--spacing) * 3);
}
@@ -524,6 +536,12 @@
max-width: 96rem;
}
}
.-m-1 {
margin: calc(var(--spacing) * -1);
}
.-m-2 {
margin: calc(var(--spacing) * -2);
}
.-mx-1\.5 {
margin-inline: calc(var(--spacing) * -1.5);
}
@@ -692,6 +710,12 @@
.table {
display: table;
}
.aspect-\[4\/3\] {
aspect-ratio: 4/3;
}
.aspect-\[16\/9\] {
aspect-ratio: 16/9;
}
.aspect-square {
aspect-ratio: 1 / 1;
}
@@ -785,12 +809,21 @@
.min-h-20 {
min-height: calc(var(--spacing) * 20);
}
.min-h-\[24px\] {
min-height: 24px;
}
.min-h-\[44px\] {
min-height: 44px;
}
.min-h-\[100px\] {
min-height: 100px;
}
.min-h-\[120px\] {
min-height: 120px;
}
.min-h-\[300px\] {
min-height: 300px;
}
.min-h-\[400px\] {
min-height: 400px;
}
@@ -914,6 +947,12 @@
.min-w-16 {
min-width: calc(var(--spacing) * 16);
}
.min-w-\[24px\] {
min-width: 24px;
}
.min-w-\[44px\] {
min-width: 44px;
}
.min-w-\[200px\] {
min-width: 200px;
}
@@ -1037,6 +1076,9 @@
.list-disc {
list-style-type: disc;
}
.appearance-none {
appearance: none;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@@ -1150,6 +1192,13 @@
margin-inline-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)));
}
}
.space-x-1\.5 {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
margin-inline-start: calc(calc(var(--spacing) * 1.5) * var(--tw-space-x-reverse));
margin-inline-end: calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-x-reverse)));
}
}
.space-x-2 {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
@@ -1171,13 +1220,6 @@
margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
}
}
.space-x-6 {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
margin-inline-start: calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse));
margin-inline-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)));
}
}
.space-x-8 {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
@@ -1228,6 +1270,9 @@
.rounded-md {
border-radius: var(--radius-md);
}
.rounded-sm {
border-radius: var(--radius-sm);
}
.rounded-xl {
border-radius: var(--radius-xl);
}
@@ -1583,6 +1628,12 @@
background-color: color-mix(in oklab, var(--color-white) 10%, transparent);
}
}
.bg-white\/50 {
background-color: color-mix(in srgb, #fff 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 50%, transparent);
}
}
.bg-white\/80 {
background-color: color-mix(in srgb, #fff 80%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1595,6 +1646,12 @@
background-color: color-mix(in oklab, var(--color-white) 90%, transparent);
}
}
.bg-white\/95 {
background-color: color-mix(in srgb, #fff 95%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 95%, transparent);
}
}
.bg-yellow-50 {
background-color: var(--color-yellow-50);
}
@@ -1625,6 +1682,13 @@
--tw-gradient-position: to top in oklab;
background-image: linear-gradient(var(--tw-gradient-stops));
}
.from-black\/20 {
--tw-gradient-from: color-mix(in srgb, #000 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-gradient-from: color-mix(in oklab, var(--color-black) 20%, transparent);
}
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-black\/70 {
--tw-gradient-from: color-mix(in srgb, #000 70%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1648,6 +1712,14 @@
--tw-gradient-from: var(--color-gray-50);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-gray-200 {
--tw-gradient-from: var(--color-gray-200);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-gray-400 {
--tw-gradient-from: var(--color-gray-400);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-gray-900 {
--tw-gradient-from: var(--color-gray-900);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@@ -1684,11 +1756,26 @@
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-gray-100 {
--tw-gradient-via: var(--color-gray-100);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-gray-500 {
--tw-gradient-via: var(--color-gray-500);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-purple-500 {
--tw-gradient-via: var(--color-purple-500);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-transparent {
--tw-gradient-via: transparent;
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-white {
--tw-gradient-via: var(--color-white);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
@@ -1706,6 +1793,14 @@
--tw-gradient-to: var(--color-gray-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-gray-200 {
--tw-gradient-to: var(--color-gray-200);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-gray-600 {
--tw-gradient-to: var(--color-gray-600);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-gray-700 {
--tw-gradient-to: var(--color-gray-700);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@@ -1761,9 +1856,6 @@
.object-cover {
object-fit: cover;
}
.p-0\.5 {
padding: calc(var(--spacing) * 0.5);
}
.p-1 {
padding: calc(var(--spacing) * 1);
}
@@ -1794,6 +1886,9 @@
.px-1 {
padding-inline: calc(var(--spacing) * 1);
}
.px-1\.5 {
padding-inline: calc(var(--spacing) * 1.5);
}
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
@@ -1851,6 +1946,9 @@
.pt-2 {
padding-top: calc(var(--spacing) * 2);
}
.pt-3 {
padding-top: calc(var(--spacing) * 3);
}
.pt-4 {
padding-top: calc(var(--spacing) * 4);
}
@@ -2127,6 +2225,12 @@
.text-white {
color: var(--color-white);
}
.text-white\/70 {
color: color-mix(in srgb, #fff 70%, transparent);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-white) 70%, transparent);
}
}
.text-yellow-400 {
color: var(--color-yellow-400);
}
@@ -2174,9 +2278,15 @@
.opacity-50 {
opacity: 50%;
}
.opacity-60 {
opacity: 60%;
}
.opacity-75 {
opacity: 75%;
}
.opacity-80 {
opacity: 80%;
}
.opacity-100 {
opacity: 100%;
}
@@ -2330,6 +2440,10 @@
--tw-duration: 300ms;
transition-duration: 300ms;
}
.duration-500 {
--tw-duration: 500ms;
transition-duration: 500ms;
}
.ease-in {
--tw-ease: var(--ease-in);
transition-timing-function: var(--ease-in);
@@ -2354,6 +2468,10 @@
--tw-outline-style: none;
outline-style: none;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
.\[coverage\:report\] {
coverage: report;
}
@@ -2381,6 +2499,16 @@
}
}
}
.group-hover\:scale-110 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
--tw-scale-x: 110%;
--tw-scale-y: 110%;
--tw-scale-z: 110%;
scale: var(--tw-scale-x) var(--tw-scale-y);
}
}
}
.group-hover\:text-blue-600 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
@@ -2983,6 +3111,11 @@
border-color: transparent;
}
}
.focus\:bg-gray-50 {
&:focus {
background-color: var(--color-gray-50);
}
}
.focus\:bg-gray-100 {
&:focus {
background-color: var(--color-gray-100);
@@ -3128,6 +3261,26 @@
opacity: 50%;
}
}
.sm\:top-2 {
@media (width >= 40rem) {
top: calc(var(--spacing) * 2);
}
}
.sm\:top-3 {
@media (width >= 40rem) {
top: calc(var(--spacing) * 3);
}
}
.sm\:right-2 {
@media (width >= 40rem) {
right: calc(var(--spacing) * 2);
}
}
.sm\:right-3 {
@media (width >= 40rem) {
right: calc(var(--spacing) * 3);
}
}
.sm\:col-span-3 {
@media (width >= 40rem) {
grid-column: span 3 / span 3;
@@ -3143,16 +3296,41 @@
grid-column: span 9 / span 9;
}
}
.sm\:mt-2 {
@media (width >= 40rem) {
margin-top: calc(var(--spacing) * 2);
}
}
.sm\:mb-0 {
@media (width >= 40rem) {
margin-bottom: calc(var(--spacing) * 0);
}
}
.sm\:mb-3 {
@media (width >= 40rem) {
margin-bottom: calc(var(--spacing) * 3);
}
}
.sm\:mb-4 {
@media (width >= 40rem) {
margin-bottom: calc(var(--spacing) * 4);
}
}
.sm\:mb-8 {
@media (width >= 40rem) {
margin-bottom: calc(var(--spacing) * 8);
}
}
.sm\:mb-16 {
@media (width >= 40rem) {
margin-bottom: calc(var(--spacing) * 16);
}
}
.sm\:ml-2 {
@media (width >= 40rem) {
margin-left: calc(var(--spacing) * 2);
}
}
.sm\:block {
@media (width >= 40rem) {
display: block;
@@ -3173,16 +3351,76 @@
display: inline;
}
}
.sm\:aspect-\[4\/3\] {
@media (width >= 40rem) {
aspect-ratio: 4/3;
}
}
.sm\:h-5 {
@media (width >= 40rem) {
height: calc(var(--spacing) * 5);
}
}
.sm\:h-10 {
@media (width >= 40rem) {
height: calc(var(--spacing) * 10);
}
}
.sm\:min-h-0 {
@media (width >= 40rem) {
min-height: calc(var(--spacing) * 0);
}
}
.sm\:min-h-\[400px\] {
@media (width >= 40rem) {
min-height: 400px;
}
}
.sm\:min-h-auto {
@media (width >= 40rem) {
min-height: auto;
}
}
.sm\:w-5 {
@media (width >= 40rem) {
width: calc(var(--spacing) * 5);
}
}
.sm\:w-10 {
@media (width >= 40rem) {
width: calc(var(--spacing) * 10);
}
}
.sm\:w-32 {
@media (width >= 40rem) {
width: calc(var(--spacing) * 32);
}
}
.sm\:w-96 {
@media (width >= 40rem) {
width: calc(var(--spacing) * 96);
}
}
.sm\:w-auto {
@media (width >= 40rem) {
width: auto;
}
}
.sm\:min-w-0 {
@media (width >= 40rem) {
min-width: calc(var(--spacing) * 0);
}
}
.sm\:flex-1 {
@media (width >= 40rem) {
flex: 1;
}
}
.sm\:flex-none {
@media (width >= 40rem) {
flex: none;
}
}
.sm\:grid-cols-1 {
@media (width >= 40rem) {
grid-template-columns: repeat(1, minmax(0, 1fr));
@@ -3198,6 +3436,11 @@
grid-template-columns: repeat(12, minmax(0, 1fr));
}
}
.sm\:flex-col {
@media (width >= 40rem) {
flex-direction: column;
}
}
.sm\:flex-row {
@media (width >= 40rem) {
flex-direction: row;
@@ -3218,6 +3461,62 @@
justify-content: flex-end;
}
}
.sm\:justify-start {
@media (width >= 40rem) {
justify-content: flex-start;
}
}
.sm\:gap-3 {
@media (width >= 40rem) {
gap: calc(var(--spacing) * 3);
}
}
.sm\:gap-4 {
@media (width >= 40rem) {
gap: calc(var(--spacing) * 4);
}
}
.sm\:gap-6 {
@media (width >= 40rem) {
gap: calc(var(--spacing) * 6);
}
}
.sm\:space-y-0 {
@media (width >= 40rem) {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));
}
}
}
.sm\:space-y-3 {
@media (width >= 40rem) {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));
}
}
}
.sm\:space-y-6 {
@media (width >= 40rem) {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
}
}
}
.sm\:space-x-2 {
@media (width >= 40rem) {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
margin-inline-start: calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));
margin-inline-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)));
}
}
}
.sm\:space-x-4 {
@media (width >= 40rem) {
:where(& > :not(:last-child)) {
@@ -3236,11 +3535,106 @@
}
}
}
.sm\:p-0\.5 {
@media (width >= 40rem) {
padding: calc(var(--spacing) * 0.5);
}
}
.sm\:p-4 {
@media (width >= 40rem) {
padding: calc(var(--spacing) * 4);
}
}
.sm\:p-6 {
@media (width >= 40rem) {
padding: calc(var(--spacing) * 6);
}
}
.sm\:px-0 {
@media (width >= 40rem) {
padding-inline: calc(var(--spacing) * 0);
}
}
.sm\:px-2 {
@media (width >= 40rem) {
padding-inline: calc(var(--spacing) * 2);
}
}
.sm\:px-2\.5 {
@media (width >= 40rem) {
padding-inline: calc(var(--spacing) * 2.5);
}
}
.sm\:px-4 {
@media (width >= 40rem) {
padding-inline: calc(var(--spacing) * 4);
}
}
.sm\:px-6 {
@media (width >= 40rem) {
padding-inline: calc(var(--spacing) * 6);
}
}
.sm\:py-0 {
@media (width >= 40rem) {
padding-block: calc(var(--spacing) * 0);
}
}
.sm\:py-1 {
@media (width >= 40rem) {
padding-block: calc(var(--spacing) * 1);
}
}
.sm\:py-2 {
@media (width >= 40rem) {
padding-block: calc(var(--spacing) * 2);
}
}
.sm\:py-6 {
@media (width >= 40rem) {
padding-block: calc(var(--spacing) * 6);
}
}
.sm\:pt-4 {
@media (width >= 40rem) {
padding-top: calc(var(--spacing) * 4);
}
}
.sm\:text-left {
@media (width >= 40rem) {
text-align: left;
}
}
.sm\:text-3xl {
@media (width >= 40rem) {
font-size: var(--text-3xl);
line-height: var(--tw-leading, var(--text-3xl--line-height));
}
}
.sm\:text-base {
@media (width >= 40rem) {
font-size: var(--text-base);
line-height: var(--tw-leading, var(--text-base--line-height));
}
}
.sm\:text-lg {
@media (width >= 40rem) {
font-size: var(--text-lg);
line-height: var(--tw-leading, var(--text-lg--line-height));
}
}
.sm\:text-sm {
@media (width >= 40rem) {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
}
.sm\:text-xl {
@media (width >= 40rem) {
font-size: var(--text-xl);
line-height: var(--tw-leading, var(--text-xl--line-height));
}
}
.md\:col-span-1 {
@media (width >= 48rem) {
grid-column: span 1 / span 1;
@@ -3296,6 +3690,11 @@
width: calc(var(--spacing) * 7);
}
}
.md\:w-40 {
@media (width >= 48rem) {
width: calc(var(--spacing) * 40);
}
}
.md\:w-48 {
@media (width >= 48rem) {
width: calc(var(--spacing) * 48);
@@ -3409,6 +3808,11 @@
width: calc(3/4 * 100%);
}
}
.lg\:w-48 {
@media (width >= 64rem) {
width: calc(var(--spacing) * 48);
}
}
.lg\:max-w-2xl {
@media (width >= 64rem) {
max-width: var(--container-2xl);
@@ -3459,6 +3863,11 @@
justify-content: space-between;
}
}
.lg\:gap-6 {
@media (width >= 64rem) {
gap: calc(var(--spacing) * 6);
}
}
.lg\:gap-8 {
@media (width >= 64rem) {
gap: calc(var(--spacing) * 8);
@@ -3469,6 +3878,11 @@
padding: calc(var(--spacing) * 6);
}
}
.lg\:px-6 {
@media (width >= 64rem) {
padding-inline: calc(var(--spacing) * 6);
}
}
.lg\:px-8 {
@media (width >= 64rem) {
padding-inline: calc(var(--spacing) * 8);
@@ -3680,11 +4094,6 @@
border-color: var(--color-green-800);
}
}
.dark\:border-purple-800 {
@media (prefers-color-scheme: dark) {
border-color: var(--color-purple-800);
}
}
.dark\:border-purple-800\/50 {
@media (prefers-color-scheme: dark) {
border-color: color-mix(in srgb, oklch(43.8% 0.218 303.724) 50%, transparent);
@@ -3871,14 +4280,6 @@
background-color: var(--color-purple-900);
}
}
.dark\:bg-purple-900\/20 {
@media (prefers-color-scheme: dark) {
background-color: color-mix(in srgb, oklch(38.1% 0.176 304.987) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-purple-900) 20%, transparent);
}
}
}
.dark\:bg-purple-900\/30 {
@media (prefers-color-scheme: dark) {
background-color: color-mix(in srgb, oklch(38.1% 0.176 304.987) 30%, transparent);
@@ -3995,6 +4396,18 @@
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
.dark\:from-gray-600 {
@media (prefers-color-scheme: dark) {
--tw-gradient-from: var(--color-gray-600);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
.dark\:from-gray-700 {
@media (prefers-color-scheme: dark) {
--tw-gradient-from: var(--color-gray-700);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
.dark\:from-gray-900 {
@media (prefers-color-scheme: dark) {
--tw-gradient-from: var(--color-gray-900);
@@ -4037,6 +4450,20 @@
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
.dark\:via-gray-600 {
@media (prefers-color-scheme: dark) {
--tw-gradient-via: var(--color-gray-600);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
}
.dark\:via-gray-700 {
@media (prefers-color-scheme: dark) {
--tw-gradient-via: var(--color-gray-700);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
}
.dark\:via-gray-800 {
@media (prefers-color-scheme: dark) {
--tw-gradient-via: var(--color-gray-800);
@@ -4057,6 +4484,18 @@
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
.dark\:to-gray-700 {
@media (prefers-color-scheme: dark) {
--tw-gradient-to: var(--color-gray-700);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
.dark\:to-gray-800 {
@media (prefers-color-scheme: dark) {
--tw-gradient-to: var(--color-gray-800);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
}
.dark\:to-gray-900 {
@media (prefers-color-scheme: dark) {
--tw-gradient-to: var(--color-gray-900);
@@ -4644,13 +5083,6 @@
}
}
}
.dark\:focus\:border-blue-500 {
@media (prefers-color-scheme: dark) {
&:focus {
border-color: var(--color-blue-500);
}
}
}
.dark\:focus\:bg-gray-700 {
@media (prefers-color-scheme: dark) {
&:focus {
@@ -4679,6 +5111,13 @@
}
}
}
.dark\:focus\:ring-offset-gray-800 {
@media (prefers-color-scheme: dark) {
&:focus {
--tw-ring-offset-color: var(--color-gray-800);
}
}
}
}
.site-logo {
font-size: 1.5rem;

View File

@@ -104,7 +104,7 @@ Features:
name="search"
x-model="search"
placeholder="{{ placeholder }}"
class="block w-full pl-10 pr-12 py-3 border border-gray-300 rounded-lg leading-5 bg-white text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 transition-colors"
class="block w-full pl-10 pr-12 py-3 border border-gray-300 dark:border-gray-600 rounded-lg leading-5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 text-base sm:text-sm min-h-[44px] sm:min-h-0"
hx-get="{% url 'parks:park_list' %}"
hx-trigger="keyup changed delay:{{ debounce_delay }}ms"
hx-target="#park-results"

View File

@@ -31,9 +31,9 @@ Features:
<div class="flex flex-wrap gap-2 {{ class }}">
{% for filter_name, filter_value in filters.items %}
{% if filter_value and filter_name != 'page' and filter_name != 'view_mode' %}
<div class="inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-blue-700 bg-blue-50 rounded-full border border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700/50">
<span class="capitalize">{{ filter_name|title }}:</span>
<span class="font-semibold">
<div class="inline-flex items-center gap-2 px-3 py-1.5 sm:py-1 text-sm font-medium text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/30 rounded-full border border-blue-200 dark:border-blue-700/50">
<span class="capitalize text-xs sm:text-sm">{{ filter_name|title }}:</span>
<span class="font-semibold text-xs sm:text-sm">
{% if filter_value == 'True' %}
Yes
{% elif filter_value == 'False' %}
@@ -44,7 +44,7 @@ Features:
</span>
<button
type="button"
class="ml-1 p-0.5 text-blue-600 hover:text-blue-800 hover:bg-blue-100 rounded-full dark:text-blue-300 dark:hover:text-blue-200 dark:hover:bg-blue-800/50 transition-colors"
class="ml-1 p-1 sm:p-0.5 text-blue-600 dark:text-blue-300 hover:text-blue-800 dark:hover:text-blue-200 hover:bg-blue-100 dark:hover:bg-blue-800/50 rounded-full transition-all duration-200 min-w-[24px] min-h-[24px] sm:min-w-0 sm:min-h-0 flex items-center justify-center"
hx-get="{% if base_url %}{{ base_url }}{% else %}{{ request.path }}{% endif %}?{% for name, value in request.GET.items %}{% if name != filter_name and name != 'page' %}{{ name }}={{ value }}&{% endif %}{% endfor %}"
hx-target="#park-results"
hx-push-url="true"

View File

@@ -48,103 +48,219 @@ Features:
{% if park %}
{% if view_mode == 'list' %}
{# Enhanced List View Item #}
{# Enhanced List View Item with CloudFlare Images #}
<article class="group bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-[1.02] overflow-hidden {{ class }}">
<div class="p-6">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
{# Main Content Section #}
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between mb-3">
<h2 class="text-xl lg:text-2xl font-bold">
{% if park.slug %}
<a href="{% url 'parks:park_detail' park.slug %}"
class="bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent hover:from-blue-600 hover:to-purple-600 dark:hover:from-blue-400 dark:hover:to-purple-400 transition-all duration-300">
{{ park.name }}
</a>
{% else %}
<span class="bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent">
{{ park.name }}
</span>
{% endif %}
</h2>
<div class="p-4 sm:p-6">
<div class="flex flex-col sm:flex-row gap-4 sm:gap-6">
{# Enhanced List View Image Section #}
<div class="flex-shrink-0 w-full sm:w-32 md:w-40 lg:w-48">
<div class="relative aspect-[16/9] sm:aspect-[4/3] bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 rounded-lg overflow-hidden">
{% if park.card_image.image or park.photos.first.image %}
{% with image=park.card_image.image|default:park.photos.first.image %}
{# List View CloudFlare Images Optimization #}
<picture class="w-full h-full">
{# Mobile list view (full width, 16:9) #}
<source media="(max-width: 639px)"
srcset="
{{ image.public_url }} 1x,
{{ image.public_url }} 2x
"
type="image/webp">
{# Tablet/Desktop list view (smaller thumbnail) #}
<source media="(min-width: 640px)"
srcset="
{{ image.public_url }} 1x,
{{ image.public_url }} 2x
"
type="image/webp">
{# Fallback image #}
<img src="{{ image.public_url }}"
alt="{{ park.name }} - {% if park.card_image.alt_text %}{{ park.card_image.alt_text }}{% elif park.photos.first.alt_text %}{{ park.photos.first.alt_text }}{% else %}Theme park exterior view{% endif %}"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
loading="lazy"
decoding="async">
</picture>
{% endwith %}
{% else %}
{# Enhanced List View Fallback #}
<div class="flex items-center justify-center h-full text-white/70 bg-gradient-to-br from-gray-400 via-gray-500 to-gray-600 dark:from-gray-600 dark:via-gray-700 dark:to-gray-800">
<svg class="w-8 h-8 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
</div>
{% endif %}
{# Status Badge #}
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border
{% if park.status == 'operating' or park.status == 'OPERATING' %}bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800
{% elif park.status == 'closed' or park.status == 'CLOSED_PERM' or park.status == 'closed_permanently' or park.status == 'closed_perm' %}bg-red-50 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800
{% elif park.status == 'seasonal' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800
{% elif park.status == 'closed_temp' or park.status == 'CLOSED_TEMP' %}bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800
{% elif park.status == 'under_construction' or park.status == 'UNDER_CONSTRUCTION' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800
{% elif park.status == 'demolished' or park.status == 'DEMOLISHED' %}bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600
{% elif park.status == 'relocated' or park.status == 'RELOCATED' %}bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/20 dark:text-purple-400 dark:border-purple-800
{% else %}bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600{% endif %}">
{{ park.get_status_display }}
</span>
</div>
{% if park.operator %}
<div class="text-base font-medium text-gray-600 dark:text-gray-400 mb-3">
{{ park.operator.name }}
{# List View Status Badge Overlay #}
<div class="absolute top-1.5 right-1.5 sm:top-2 sm:right-2">
<span class="inline-flex items-center px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full text-xs font-semibold border shrink-0 bg-white/95 backdrop-blur-sm shadow-sm
{% if park.status == 'operating' or park.status == 'OPERATING' %}text-green-700 border-green-200
{% elif park.status == 'closed' or park.status == 'CLOSED_PERM' or park.status == 'closed_permanently' or park.status == 'closed_perm' %}text-red-700 border-red-200
{% elif park.status == 'seasonal' %}text-blue-700 border-blue-200
{% elif park.status == 'closed_temp' or park.status == 'CLOSED_TEMP' %}text-yellow-700 border-yellow-200
{% elif park.status == 'under_construction' or park.status == 'UNDER_CONSTRUCTION' %}text-blue-700 border-blue-200
{% elif park.status == 'demolished' or park.status == 'DEMOLISHED' %}text-gray-700 border-gray-200
{% elif park.status == 'relocated' or park.status == 'RELOCATED' %}text-purple-700 border-purple-200
{% else %}text-gray-700 border-gray-200{% endif %}">
<span class="hidden sm:inline">{{ park.get_status_display }}</span>
<span class="sm:hidden">{{ park.get_status_display|truncatechars:3 }}</span>
</span>
</div>
{% endif %}
{% if park.description %}
<p class="text-gray-600 dark:text-gray-400 line-clamp-2 mb-4">
{{ park.description|truncatewords:30 }}
</p>
{% endif %}
</div>
</div>
{# Stats Section #}
{% if park.ride_count or park.coaster_count %}
<div class="flex items-center space-x-6 text-sm">
{% if park.ride_count %}
<div class="flex items-center space-x-2 px-4 py-2 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg border border-blue-200/50 dark:border-blue-800/50">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
{# Enhanced Main Content Section with Better Mobile Layout #}
<div class="flex-1 min-w-0 flex flex-col justify-between">
<div class="space-y-2 sm:space-y-3">
{# Enhanced Title with Better Mobile Typography #}
<div class="flex items-start justify-between">
<h2 class="text-lg sm:text-xl lg:text-2xl font-bold line-clamp-2 leading-tight">
{% if park.slug %}
<a href="{% url 'parks:park_detail' park.slug %}"
class="bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent hover:from-blue-600 hover:to-purple-600 dark:hover:from-blue-400 dark:hover:to-purple-400 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-sm"
aria-label="View details for {{ park.name }}">
{{ park.name }}
</a>
{% else %}
<span class="bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 bg-clip-text text-transparent">
{{ park.name }}
</span>
{% endif %}
</h2>
{# View Details Arrow for Mobile #}
<div class="sm:hidden text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 ml-2 flex-shrink-0">
<svg class="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
<span class="font-semibold text-blue-700 dark:text-blue-300">{{ park.ride_count }}</span>
<span class="text-blue-600 dark:text-blue-400">rides</span>
</div>
</div>
{# Enhanced Operator Display #}
{% if park.operator %}
<div class="text-sm sm:text-base font-medium text-gray-600 dark:text-gray-400 flex items-center">
<svg class="w-3 h-3 mr-1.5 flex-shrink-0 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg>
<span class="truncate">{{ park.operator.name }}</span>
</div>
{% endif %}
{% if park.coaster_count %}
<div class="flex items-center space-x-2 px-4 py-2 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-lg border border-purple-200/50 dark:border-purple-800/50">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<span class="font-semibold text-purple-700 dark:text-purple-300">{{ park.coaster_count }}</span>
<span class="text-purple-600 dark:text-purple-400">coasters</span>
</div>
{# Enhanced Description #}
{% if park.description %}
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 leading-relaxed">
{{ park.description|truncatewords:30 }}
</p>
{% endif %}
</div>
{% endif %}
{# Enhanced Stats Section with Better Mobile Layout #}
{% if park.ride_count or park.coaster_count %}
<div class="flex items-center justify-between pt-3 border-t border-gray-200/50 dark:border-gray-600/50 mt-3">
<div class="flex items-center space-x-3 sm:space-x-6 text-sm">
{% if park.ride_count %}
<div class="flex items-center space-x-1.5 sm:space-x-2 px-2 sm:px-4 py-1.5 sm:py-2 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg border border-blue-200/50 dark:border-blue-800/50" title="{{ park.ride_count }} ride{{ park.ride_count|pluralize }}">
<svg class="w-4 h-4 sm:w-5 sm:h-5 text-blue-600 dark:text-blue-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
<span class="font-semibold text-blue-700 dark:text-blue-300">{{ park.ride_count }}</span>
<span class="text-blue-600 dark:text-blue-400 hidden sm:inline">rides</span>
</div>
{% endif %}
{% if park.coaster_count %}
<div class="flex items-center space-x-1.5 sm:space-x-2 px-2 sm:px-4 py-1.5 sm:py-2 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-lg border border-purple-200/50 dark:border-purple-800/50" title="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }}">
<svg class="w-4 h-4 sm:w-5 sm:h-5 text-purple-600 dark:text-purple-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<span class="font-semibold text-purple-700 dark:text-purple-300">{{ park.coaster_count }}</span>
<span class="text-purple-600 dark:text-purple-400 hidden sm:inline">coasters</span>
</div>
{% endif %}
</div>
{# View Details Arrow for Desktop #}
<div class="hidden sm:block text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200">
<svg class="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</div>
{% else %}
{# Show arrow even when no stats for consistent layout #}
<div class="hidden sm:flex justify-end pt-3 border-t border-gray-200/50 dark:border-gray-600/50 mt-3">
<div class="text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200">
<svg class="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</article>
{% else %}
{# Enhanced Grid View Item #}
<article class="group bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 hover:-rotate-1 overflow-hidden {{ class }}">
{# Park Image #}
<div class="relative h-48 bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500">
{% if park.card_image %}
<img src="{{ park.card_image.image.url }}"
alt="{{ park.name }}"
class="w-full h-full object-cover">
{% elif park.photos.first %}
<img src="{{ park.photos.first.image.url }}"
alt="{{ park.name }}"
class="w-full h-full object-cover">
{# Enhanced Park Image with CloudFlare Images Integration #}
<div class="relative aspect-[4/3] bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 overflow-hidden">
{% if park.card_image.image or park.photos.first.image %}
{% with image=park.card_image.image|default:park.photos.first.image %}
{# CloudFlare Images Responsive Picture Element #}
<picture class="w-full h-full">
{# Mobile optimization (320-767px) - thumbnail variant with mobile-specific transformations #}
<source media="(max-width: 767px)"
srcset="
{{ image.public_url }} 1x,
{{ image.public_url }} 2x
"
type="image/webp">
{# Tablet optimization (768-1023px) - medium variant #}
<source media="(min-width: 768px) and (max-width: 1023px)"
srcset="
{{ image.public_url }} 1x,
{{ image.public_url }} 2x
"
type="image/webp">
{# Desktop optimization (1024px+) - large variant #}
<source media="(min-width: 1024px)"
srcset="
{{ image.public_url }} 1x,
{{ image.public_url }} 2x
"
type="image/webp">
{# Fallback image with progressive enhancement #}
<img src="{{ image.public_url }}"
alt="{{ park.name }} - {% if park.card_image.alt_text %}{{ park.card_image.alt_text }}{% elif park.photos.first.alt_text %}{{ park.photos.first.alt_text }}{% else %}Theme park exterior view{% endif %}"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
loading="lazy"
decoding="async"
style="aspect-ratio: 4/3; object-position: center;">
</picture>
{# Image Overlay Effects #}
<div class="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{% endwith %}
{% else %}
<div class="flex items-center justify-center h-full">
<svg class="w-16 h-16 text-white opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
{# Enhanced Fallback with Better UX #}
<div class="flex flex-col items-center justify-center h-full text-white/70 bg-gradient-to-br from-gray-400 via-gray-500 to-gray-600 dark:from-gray-600 dark:via-gray-700 dark:to-gray-800">
<div class="p-6 text-center">
<svg class="w-12 h-12 mx-auto mb-3 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
<p class="text-sm font-medium opacity-80">No Image Available</p>
<p class="text-xs opacity-60 mt-1">{{ park.name }}</p>
</div>
</div>
{% endif %}
{# Status Badge Overlay #}
<div class="absolute top-3 right-3">
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold border shrink-0 bg-white/90 backdrop-blur-sm
{# Enhanced Status Badge Overlay with Better Mobile Touch Targets #}
<div class="absolute top-2 right-2 sm:top-3 sm:right-3">
<span class="inline-flex items-center px-2 py-1 sm:px-2.5 sm:py-1 rounded-full text-xs font-semibold border shrink-0 bg-white/95 backdrop-blur-sm shadow-sm
{% if park.status == 'operating' or park.status == 'OPERATING' %}text-green-700 border-green-200
{% elif park.status == 'closed' or park.status == 'CLOSED_PERM' or park.status == 'closed_permanently' or park.status == 'closed_perm' %}text-red-700 border-red-200
{% elif park.status == 'seasonal' %}text-blue-700 border-blue-200
@@ -156,14 +272,20 @@ Features:
{{ park.get_status_display }}
</span>
</div>
{# Loading Placeholder with Skeleton Effect #}
<div class="absolute inset-0 bg-gradient-to-r from-gray-200 via-gray-100 to-gray-200 dark:from-gray-700 dark:via-gray-600 dark:to-gray-700 animate-pulse opacity-0 transition-opacity duration-300" data-loading-placeholder></div>
</div>
<div class="p-6">
<div class="mb-4">
<h2 class="text-xl font-bold line-clamp-2 mb-2">
{# Enhanced Content Area with Better Mobile Optimization #}
<div class="p-4 sm:p-6">
<div class="mb-3 sm:mb-4">
{# Enhanced Title with Better Mobile Typography #}
<h2 class="text-lg sm:text-xl font-bold line-clamp-2 mb-2 leading-tight">
{% if park.slug %}
<a href="{% url 'parks:park_detail' park.slug %}"
class="text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-300">
class="text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-sm"
aria-label="View details for {{ park.name }}">
{{ park.name }}
</a>
{% else %}
@@ -174,43 +296,59 @@ Features:
</h2>
</div>
{# Enhanced Operator Display with Better Mobile Layout #}
{% if park.operator %}
<div class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-3 truncate">
{{ park.operator.name }}
<div class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-3 truncate flex items-center">
<svg class="w-3 h-3 mr-1.5 flex-shrink-0 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg>
<span class="truncate">{{ park.operator.name }}</span>
</div>
{% endif %}
{# Enhanced Description with Better Mobile Readability #}
{% if park.description %}
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-3 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-3 mb-4 leading-relaxed">
{{ park.description|truncatewords:15 }}
</p>
{% endif %}
{# Stats Footer #}
{# Enhanced Stats Footer with Better Mobile Layout #}
{% if park.ride_count or park.coaster_count %}
<div class="flex items-center justify-between pt-4 border-t border-gray-200/50 dark:border-gray-600/50">
<div class="flex items-center space-x-4 text-sm">
<div class="flex items-center justify-between pt-3 sm:pt-4 border-t border-gray-200/50 dark:border-gray-600/50">
<div class="flex items-center space-x-3 sm:space-x-4 text-sm">
{% if park.ride_count %}
<div class="flex items-center space-x-1 text-blue-600 dark:text-blue-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="flex items-center space-x-1.5 text-blue-600 dark:text-blue-400" title="{{ park.ride_count }} ride{{ park.ride_count|pluralize }}">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
<span class="font-semibold">{{ park.ride_count }}</span>
<span class="hidden sm:inline text-xs opacity-75">rides</span>
</div>
{% endif %}
{% if park.coaster_count %}
<div class="flex items-center space-x-1 text-purple-600 dark:text-purple-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="flex items-center space-x-1.5 text-purple-600 dark:text-purple-400" title="{{ park.coaster_count }} roller coaster{{ park.coaster_count|pluralize }}">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<span class="font-semibold">{{ park.coaster_count }}</span>
<span class="hidden sm:inline text-xs opacity-75">coasters</span>
</div>
{% endif %}
</div>
{# View Details Arrow #}
<div class="text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200">
<svg class="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{# Enhanced View Details Arrow with Better Mobile Touch Target #}
<div class="text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 p-1 -m-1">
<svg class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</div>
{% else %}
{# Show arrow even when no stats for consistent layout #}
<div class="flex justify-end pt-3 sm:pt-4 border-t border-gray-200/50 dark:border-gray-600/50">
<div class="text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 p-1 -m-1">
<svg class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>

View File

@@ -37,7 +37,7 @@ Features:
<div>
<button
type="button"
class="inline-flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700"
class="inline-flex items-center justify-center w-full px-3 sm:px-4 py-2.5 sm:py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 min-h-[44px] sm:min-h-0"
@click="open = !open"
aria-expanded="true"
aria-haspopup="true"

View File

@@ -33,7 +33,7 @@ Features:
<div class="inline-flex rounded-lg border border-gray-200 dark:border-gray-700 {{ class }}" role="group" aria-label="View toggle">
<button
type="button"
class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-l-lg transition-colors {% if current_view == 'grid' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700{% else %}bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700{% endif %}"
class="inline-flex items-center px-3 py-2.5 sm:py-2 text-sm font-medium rounded-l-lg transition-all duration-200 min-h-[44px] sm:min-h-0 {% if current_view == 'grid' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700{% else %}bg-white text-gray-700 hover:bg-gray-50 focus:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:bg-gray-700{% endif %} focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
hx-get="{% url 'parks:park_list' %}?{% for name, value in request.GET.items %}{% if name != 'view_mode' and name != 'page' %}{{ name }}={{ value }}&{% endif %}{% endfor %}view_mode=grid"
hx-target="#park-results"
hx-push-url="true"
@@ -50,7 +50,7 @@ Features:
<button
type="button"
class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-r-lg transition-colors {% if current_view == 'list' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700{% else %}bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700{% endif %}"
class="inline-flex items-center px-3 py-2.5 sm:py-2 text-sm font-medium rounded-r-lg transition-all duration-200 min-h-[44px] sm:min-h-0 {% if current_view == 'list' %}bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700{% else %}bg-white text-gray-700 hover:bg-gray-50 focus:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:bg-gray-700{% endif %} focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
hx-get="{% url 'parks:park_list' %}?{% for name, value in request.GET.items %}{% if name != 'view_mode' and name != 'page' %}{{ name }}={{ value }}&{% endif %}{% endfor %}view_mode=list"
hx-target="#park-results"
hx-push-url="true"

View File

@@ -5,43 +5,45 @@
{% block title %}Parks{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-6" x-data="parkListState()">
<!-- Enhanced Header Section -->
<div class="mb-8">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
<div>
<h1 class="text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white">
{# Enhanced Mobile-First Container with Better Spacing #}
<div class="container mx-auto px-3 sm:px-4 lg:px-6 py-4 sm:py-6" x-data="parkListState()">
{# Enhanced Mobile-First Header Section #}
<div class="mb-6 sm:mb-8">
<div class="flex flex-col gap-4 sm:gap-6">
{# Enhanced Mobile-First Title Section #}
<div class="text-center sm:text-left">
<h1 class="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white leading-tight">
Theme Parks
</h1>
<p class="mt-2 text-lg text-gray-600 dark:text-gray-400">
<p class="mt-1 sm:mt-2 text-base sm:text-lg text-gray-600 dark:text-gray-400">
Discover amazing theme parks around the world
</p>
</div>
<!-- Quick Stats -->
<div class="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
<div class="text-center">
<div class="font-bold text-lg text-gray-900 dark:text-white">{{ filter_counts.total_parks|default:0 }}</div>
<div>Total Parks</div>
{# Enhanced Mobile-First Quick Stats with Better Touch Targets #}
<div class="grid grid-cols-3 gap-3 sm:gap-4 lg:gap-6">
<div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50">
<div class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white">{{ filter_counts.total_parks|default:0 }}</div>
<div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">Total Parks</div>
</div>
<div class="text-center">
<div class="font-bold text-lg text-gray-900 dark:text-white">{{ filter_counts.operating_parks|default:0 }}</div>
<div>Operating</div>
<div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50">
<div class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white">{{ filter_counts.operating_parks|default:0 }}</div>
<div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">Operating</div>
</div>
<div class="text-center">
<div class="font-bold text-lg text-gray-900 dark:text-white">{{ filter_counts.parks_with_coasters|default:0 }}</div>
<div>With Coasters</div>
<div class="text-center p-3 sm:p-4 bg-white/50 dark:bg-gray-800/50 rounded-lg border border-gray-200/50 dark:border-gray-700/50">
<div class="font-bold text-lg sm:text-xl lg:text-2xl text-gray-900 dark:text-white">{{ filter_counts.parks_with_coasters|default:0 }}</div>
<div class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1">With Coasters</div>
</div>
</div>
</div>
</div>
<!-- Enhanced Search and Filter Bar -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-6 mb-8">
<div class="space-y-6">
<!-- Main Search Row -->
<div class="flex flex-col lg:flex-row gap-4">
<!-- Enhanced Search Input -->
{# Enhanced Mobile-First Search and Filter Bar #}
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-4 sm:p-6 mb-6 sm:mb-8">
<div class="space-y-4 sm:space-y-6">
{# Enhanced Mobile-First Main Search Row #}
<div class="space-y-3 sm:space-y-0 sm:flex sm:flex-col lg:flex-row gap-4">
{# Enhanced Search Input with Better Mobile UX #}
<div class="flex-1">
<c-enhanced_search
placeholder="Search parks by name, location, or features..."
@@ -51,53 +53,62 @@
/>
</div>
<!-- Controls Row -->
<div class="flex items-center gap-3">
<!-- Sort Controls -->
<c-sort_controls
current_sort="{{ current_ordering }}"
class="min-w-0"
/>
{# Enhanced Mobile-First Controls Row with Better Touch Targets #}
<div class="flex items-center justify-between sm:justify-start gap-2 sm:gap-3">
{# Sort Controls with Mobile Optimization #}
<div class="flex-1 sm:flex-none min-w-0">
<c-sort_controls
current_sort="{{ current_ordering }}"
class="w-full sm:w-auto"
/>
</div>
<!-- View Toggle -->
<c-view_toggle
current_view="{{ view_mode }}"
class="flex-shrink-0"
/>
{# View Toggle with Better Mobile Touch Target #}
<div class="flex-shrink-0">
<c-view_toggle
current_view="{{ view_mode }}"
class=""
/>
</div>
<!-- Filter Toggle Button (Mobile) -->
{# Enhanced Mobile Filter Toggle Button with Better Design #}
<button
type="button"
class="lg:hidden inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-600"
class="lg:hidden inline-flex items-center px-3 py-2.5 sm:px-4 sm:py-2 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm bg-white dark:bg-gray-700 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200 min-w-[44px] min-h-[44px] justify-center"
@click="showFilters = !showFilters"
:aria-expanded="showFilters"
aria-label="Toggle filters"
:class="{ 'bg-blue-50 dark:bg-blue-900/20 border-blue-300 dark:border-blue-600 text-blue-700 dark:text-blue-300': showFilters }"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-4 h-4 sm:w-5 sm:h-5 transition-transform duration-200"
:class="{ 'rotate-180': showFilters }"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
<span class="ml-1">Filters</span>
<span class="ml-1 sm:ml-2 hidden sm:inline">Filters</span>
<span class="sr-only sm:hidden" x-text="showFilters ? 'Hide filters' : 'Show filters'"></span>
</button>
</div>
</div>
<!-- Advanced Filters Row -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
{# Enhanced Mobile-First Advanced Filters with Better Touch Interaction #}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4"
x-show="showFilters"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-95">
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-95 -translate-y-2"
x-transition:enter-end="opacity-100 transform scale-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 transform scale-100 translate-y-0"
x-transition:leave-end="opacity-0 transform scale-95 -translate-y-2">
<!-- Status Filter -->
{# Enhanced Mobile-First Status Filter #}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Status
</label>
<select
name="status"
class="block w-full px-3 py-2 border border-gray-300 rounded-md bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:focus:ring-blue-500"
class="block w-full px-3 py-3 sm:py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors duration-200 min-h-[44px] appearance-none cursor-pointer"
hx-get="{% url 'parks:park_list' %}"
hx-target="#park-results"
hx-include="[name='search'], [name='view_mode'], [name='ordering'], [name='operator'], [name='park_type']"
@@ -105,21 +116,21 @@
hx-indicator="#search-spinner"
>
<option value="">All Statuses</option>
<option value="OPERATING" {% if request.GET.status == 'OPERATING' %}selected{% endif %}>Operating</option>
<option value="CLOSED_TEMP" {% if request.GET.status == 'CLOSED_TEMP' %}selected{% endif %}>Temporarily Closed</option>
<option value="CLOSED_PERM" {% if request.GET.status == 'CLOSED_PERM' %}selected{% endif %}>Permanently Closed</option>
<option value="UNDER_CONSTRUCTION" {% if request.GET.status == 'UNDER_CONSTRUCTION' %}selected{% endif %}>Under Construction</option>
<option value="OPERATING" {% if request.GET.status == 'OPERATING' %}selected{% endif %}>🟢 Operating</option>
<option value="CLOSED_TEMP" {% if request.GET.status == 'CLOSED_TEMP' %}selected{% endif %}>🟡 Temporarily Closed</option>
<option value="CLOSED_PERM" {% if request.GET.status == 'CLOSED_PERM' %}selected{% endif %}>🔴 Permanently Closed</option>
<option value="UNDER_CONSTRUCTION" {% if request.GET.status == 'UNDER_CONSTRUCTION' %}selected{% endif %}>🚧 Under Construction</option>
</select>
</div>
<!-- Operator Filter -->
{# Enhanced Mobile-First Operator Filter #}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Operator
</label>
<select
name="operator"
class="block w-full px-3 py-2 border border-gray-300 rounded-md bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:focus:ring-blue-500"
class="block w-full px-3 py-3 sm:py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors duration-200 min-h-[44px] appearance-none cursor-pointer"
hx-get="{% url 'parks:park_list' %}"
hx-target="#park-results"
hx-include="[name='search'], [name='view_mode'], [name='ordering'], [name='status'], [name='park_type']"
@@ -136,14 +147,14 @@
</select>
</div>
<!-- Park Type Filter -->
{# Enhanced Mobile-First Park Type Filter #}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Park Type
</label>
<select
name="park_type"
class="block w-full px-3 py-2 border border-gray-300 rounded-md bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:focus:ring-blue-500"
class="block w-full px-3 py-3 sm:py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors duration-200 min-h-[44px] appearance-none cursor-pointer"
hx-get="{% url 'parks:park_list' %}"
hx-target="#park-results"
hx-include="[name='search'], [name='view_mode'], [name='ordering'], [name='status'], [name='operator']"
@@ -151,70 +162,86 @@
hx-indicator="#search-spinner"
>
<option value="">All Types</option>
<option value="disney" {% if request.GET.park_type == 'disney' %}selected{% endif %}>Disney Parks</option>
<option value="universal" {% if request.GET.park_type == 'universal' %}selected{% endif %}>Universal Parks</option>
<option value="six_flags" {% if request.GET.park_type == 'six_flags' %}selected{% endif %}>Six Flags</option>
<option value="cedar_fair" {% if request.GET.park_type == 'cedar_fair' %}selected{% endif %}>Cedar Fair</option>
<option value="independent" {% if request.GET.park_type == 'independent' %}selected{% endif %}>Independent</option>
<option value="disney" {% if request.GET.park_type == 'disney' %}selected{% endif %}>🏰 Disney Parks</option>
<option value="universal" {% if request.GET.park_type == 'universal' %}selected{% endif %}>🎬 Universal Parks</option>
<option value="six_flags" {% if request.GET.park_type == 'six_flags' %}selected{% endif %}>🎢 Six Flags</option>
<option value="cedar_fair" {% if request.GET.park_type == 'cedar_fair' %}selected{% endif %}>🌲 Cedar Fair</option>
<option value="independent" {% if request.GET.park_type == 'independent' %}selected{% endif %}>Independent</option>
</select>
</div>
<!-- Quick Filters -->
{# Enhanced Mobile-First Quick Filters with Better Touch Targets #}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Quick Filters
</label>
<div class="space-y-2">
<label class="flex items-center">
<div class="space-y-3">
<label class="flex items-center p-2 -m-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200">
<input
type="checkbox"
name="has_coasters"
value="true"
{% if request.GET.has_coasters %}checked{% endif %}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:focus:ring-offset-gray-800 transition-colors duration-200"
hx-get="{% url 'parks:park_list' %}"
hx-target="#park-results"
hx-include="[name='search'], [name='view_mode'], [name='ordering'], [name='status'], [name='operator'], [name='park_type']"
hx-push-url="true"
hx-indicator="#search-spinner"
/>
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Has Roller Coasters</span>
<span class="ml-3 text-sm text-gray-700 dark:text-gray-300 select-none">
🎢 Has Roller Coasters
</span>
</label>
<label class="flex items-center">
<label class="flex items-center p-2 -m-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200">
<input
type="checkbox"
name="big_parks_only"
value="true"
{% if request.GET.big_parks_only %}checked{% endif %}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:focus:ring-offset-gray-800 transition-colors duration-200"
hx-get="{% url 'parks:park_list' %}"
hx-target="#park-results"
hx-include="[name='search'], [name='view_mode'], [name='ordering'], [name='status'], [name='operator'], [name='park_type']"
hx-push-url="true"
hx-indicator="#search-spinner"
/>
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Major Parks (10+ rides)</span>
<span class="ml-3 text-sm text-gray-700 dark:text-gray-300 select-none">
🏢 Major Parks (10+ rides)
</span>
</label>
</div>
</div>
</div>
<!-- Active Filter Chips -->
{# Enhanced Mobile-First Active Filter Chips #}
{% if active_filters %}
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
<div class="border-t border-gray-200 dark:border-gray-700 pt-3 sm:pt-4">
<div class="flex items-center justify-between mb-2 sm:mb-3">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">
Active Filters
</h3>
<button
type="button"
@click="clearAllFilters()"
class="text-xs sm:text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium focus:outline-none focus:underline transition-colors duration-200 min-h-[44px] px-2 py-1 sm:min-h-auto sm:px-0 sm:py-0"
>
Clear All
</button>
</div>
<c-filter_chips
filters=active_filters
base_url="{% url 'parks:park_list' %}"
class="flex-wrap"
class="flex-wrap gap-2"
/>
</div>
{% endif %}
</div>
</div>
<!-- Results Section -->
<div class="space-y-6">
<!-- Results Statistics -->
{# Enhanced Mobile-First Results Section #}
<div class="space-y-4 sm:space-y-6">
{# Enhanced Mobile-First Results Statistics #}
<c-result_stats
total_results="{{ total_results }}"
page_obj="{{ page_obj }}"
@@ -223,25 +250,28 @@
filter_count="{{ filter_count }}"
/>
<!-- Loading Overlay -->
{# Enhanced Mobile-First Loading Overlay #}
<div id="loading-overlay" class="htmx-indicator">
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-xl">
<div class="flex items-center space-x-3">
<svg class="animate-spin h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24">
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white dark:bg-gray-800 rounded-xl p-4 sm:p-6 shadow-xl max-w-sm w-full">
<div class="flex flex-col items-center space-y-3 text-center">
<svg class="animate-spin h-8 w-8 sm:h-10 sm:w-10 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="text-lg font-medium text-gray-900 dark:text-white">Loading parks...</span>
<div>
<div class="text-base sm:text-lg font-medium text-gray-900 dark:text-white">Loading parks...</div>
<div class="text-sm text-gray-600 dark:text-gray-400 mt-1">Please wait a moment</div>
</div>
</div>
</div>
</div>
</div>
<!-- Park Results Container -->
{# Enhanced Mobile-First Park Results Container #}
<div id="park-results"
hx-indicator="#loading-overlay"
class="min-h-[400px]">
class="min-h-[300px] sm:min-h-[400px]">
{% include "parks/partials/park_list.html" %}
</div>
</div>
@@ -249,51 +279,110 @@
<!-- AlpineJS State Management -->
<script>
{# Enhanced Mobile-First AlpineJS State Management #}
function parkListState() {
return {
showFilters: window.innerWidth >= 1024, // Show on desktop by default
viewMode: '{{ view_mode }}',
searchQuery: '{{ search_query }}',
isLoading: false,
error: null,
init() {
// Handle responsive filter visibility
// Handle responsive filter visibility with better mobile UX
this.handleResize();
window.addEventListener('resize', () => this.handleResize());
window.addEventListener('resize', this.debounce(() => this.handleResize(), 250));
// Handle HTMX events
// Enhanced HTMX events with better mobile feedback
document.addEventListener('htmx:beforeRequest', () => {
this.setLoading(true);
this.error = null;
});
document.addEventListener('htmx:afterRequest', () => {
document.addEventListener('htmx:afterRequest', (event) => {
this.setLoading(false);
// Scroll to top of results on mobile after filter changes
if (window.innerWidth < 768 && event.detail.target?.id === 'park-results') {
this.scrollToResults();
}
});
document.addEventListener('htmx:responseError', () => {
this.setLoading(false);
this.showError('Failed to load results. Please try again.');
this.showError('Failed to load results. Please check your connection and try again.');
});
// Handle mobile viewport changes (orientation, virtual keyboard)
this.handleMobileViewport();
},
handleResize() {
if (window.innerWidth >= 1024) {
this.showFilters = true;
} else {
// Keep current state on mobile
}
// Auto-hide filters on mobile after interaction for better UX
// Keep current state but could add auto-hide logic here
},
handleMobileViewport() {
// Handle mobile viewport changes for better UX
if ('visualViewport' in window) {
window.visualViewport.addEventListener('resize', () => {
// Handle virtual keyboard appearance/disappearance
document.documentElement.style.setProperty(
'--viewport-height',
`${window.visualViewport.height}px`
);
});
}
},
scrollToResults() {
// Smooth scroll to results on mobile for better UX
const resultsElement = document.getElementById('park-results');
if (resultsElement) {
resultsElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
},
setLoading(loading) {
// Additional loading state management if needed
this.isLoading = loading;
// Disable form interactions while loading for better UX
const formElements = document.querySelectorAll('select, input, button');
formElements.forEach(el => {
el.disabled = loading;
});
},
showError(message) {
// Show error notification
this.error = message;
// Auto-clear error after 5 seconds
setTimeout(() => {
this.error = null;
}, 5000);
console.error(message);
},
clearAllFilters() {
// Add loading state for better UX
this.setLoading(true);
window.location.href = '{% url "parks:park_list" %}';
},
// Utility function for better performance
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}
}