Compare commits

...

1 Commits

Author SHA1 Message Date
Juhn-Vitus Saß
b09eb2c7d6 Fix #5 2025-03-08 15:34:39 +01:00
7 changed files with 1046 additions and 37 deletions

View File

@@ -6,26 +6,45 @@
'Noto Color Emoji';
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
--color-red-100: oklch(0.936 0.032 17.717);
--color-red-200: oklch(0.885 0.062 18.334);
--color-red-500: oklch(0.637 0.237 25.331);
--color-red-600: oklch(0.577 0.245 27.325);
--color-red-700: oklch(0.505 0.213 27.518);
--color-red-800: oklch(0.444 0.177 26.899);
--color-yellow-500: oklch(0.795 0.184 86.047);
--color-green-500: oklch(0.723 0.219 149.579);
--color-green-600: oklch(0.627 0.194 149.214);
--color-green-700: oklch(0.527 0.154 150.069);
--color-blue-100: oklch(0.932 0.032 255.585);
--color-blue-400: oklch(0.707 0.165 254.624);
--color-blue-500: oklch(0.623 0.214 259.815);
--color-blue-600: oklch(0.546 0.245 262.881);
--color-blue-700: oklch(0.488 0.243 264.376);
--color-blue-800: oklch(0.424 0.199 265.638);
--color-indigo-500: oklch(0.585 0.233 277.117);
--color-indigo-600: oklch(0.511 0.262 276.966);
--color-purple-100: oklch(0.946 0.033 307.174);
--color-purple-500: oklch(0.627 0.265 303.9);
--color-purple-800: oklch(0.438 0.218 303.724);
--color-gray-50: oklch(0.985 0.002 247.839);
--color-gray-100: oklch(0.967 0.003 264.542);
--color-gray-200: oklch(0.928 0.006 264.531);
--color-gray-300: oklch(0.872 0.01 258.338);
--color-gray-400: oklch(0.707 0.022 261.325);
--color-gray-500: oklch(0.551 0.027 264.364);
--color-gray-600: oklch(0.446 0.03 256.802);
--color-gray-700: oklch(0.373 0.034 259.733);
--color-gray-900: oklch(0.21 0.034 264.665);
--color-stone-900: oklch(0.216 0.006 56.043);
--color-white: #fff;
--spacing: 0.25rem;
--breakpoint-lg: 64rem;
--container-md: 28rem;
--container-lg: 32rem;
--container-7xl: 80rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--text-lg: 1.125rem;
@@ -40,6 +59,7 @@
--text-8xl--line-height: 1;
--font-weight-extralight: 200;
--font-weight-light: 300;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
@@ -200,12 +220,21 @@
}
}
@layer utilities {
.collapse {
visibility: collapse;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.static {
position: static;
}
.sticky {
position: sticky;
}
.top-0 {
top: calc(var(--spacing) * 0);
}
@@ -224,6 +253,24 @@
.z-40 {
z-index: 40;
}
.container {
width: 100%;
@media (width >= 40rem) {
max-width: 40rem;
}
@media (width >= 48rem) {
max-width: 48rem;
}
@media (width >= 64rem) {
max-width: 64rem;
}
@media (width >= 80rem) {
max-width: 80rem;
}
@media (width >= 96rem) {
max-width: 96rem;
}
}
.m-0 {
margin: calc(var(--spacing) * 0);
}
@@ -251,18 +298,54 @@
.mt-4 {
margin-top: calc(var(--spacing) * 4);
}
.mt-6 {
margin-top: calc(var(--spacing) * 6);
}
.mt-8 {
margin-top: calc(var(--spacing) * 8);
}
.mt-10 {
margin-top: calc(var(--spacing) * 10);
}
.mr-0 {
margin-right: calc(var(--spacing) * 0);
}
.mr-0\.75 {
margin-right: calc(var(--spacing) * 0.75);
}
.mr-1\.5 {
margin-right: calc(var(--spacing) * 1.5);
}
.mb-2 {
margin-bottom: calc(var(--spacing) * 2);
}
.mb-3 {
margin-bottom: calc(var(--spacing) * 3);
}
.mb-4 {
margin-bottom: calc(var(--spacing) * 4);
}
.mb-6 {
margin-bottom: calc(var(--spacing) * 6);
}
.mb-8 {
margin-bottom: calc(var(--spacing) * 8);
}
.ml-0 {
margin-left: calc(var(--spacing) * 0);
}
.ml-0\.75 {
margin-left: calc(var(--spacing) * 0.75);
}
.ml-1 {
margin-left: calc(var(--spacing) * 1);
}
.ml-2 {
margin-left: calc(var(--spacing) * 2);
}
.ml-4 {
margin-left: calc(var(--spacing) * 4);
}
.block {
display: block;
}
@@ -275,6 +358,21 @@
.hidden {
display: none;
}
.inline-flex {
display: inline-flex;
}
.list-item {
display: list-item;
}
.table {
display: table;
}
.h-4 {
height: calc(var(--spacing) * 4);
}
.h-5 {
height: calc(var(--spacing) * 5);
}
.h-12 {
height: calc(var(--spacing) * 12);
}
@@ -290,12 +388,51 @@
.h-full {
height: 100%;
}
.w-4 {
width: calc(var(--spacing) * 4);
}
.w-5 {
width: calc(var(--spacing) * 5);
}
.w-12 {
width: calc(var(--spacing) * 12);
}
.w-full {
width: 100%;
}
.max-w-7xl {
max-width: var(--container-7xl);
}
.max-w-lg {
max-width: var(--container-lg);
}
.max-w-screen-lg {
max-width: var(--breakpoint-lg);
}
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
.border-collapse {
border-collapse: collapse;
}
.transform {
transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
}
.cursor-pointer {
cursor: pointer;
}
.resize {
resize: both;
}
.list-inside {
list-style-position: inside;
}
.list-disc {
list-style-type: disc;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@@ -311,6 +448,9 @@
.items-center {
align-items: center;
}
.items-start {
align-items: flex-start;
}
.justify-around {
justify-content: space-around;
}
@@ -329,6 +469,20 @@
.gap-4 {
gap: calc(var(--spacing) * 4);
}
.space-y-1 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-y-3 {
: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)));
}
}
.space-y-4 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
@@ -336,6 +490,13 @@
margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-y-6 {
: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)));
}
}
.space-x-2 {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
@@ -343,6 +504,13 @@
margin-inline-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)));
}
}
.space-x-3 {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
margin-inline-start: calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));
margin-inline-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)));
}
}
.space-x-4 {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
@@ -350,12 +518,29 @@
margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
}
}
.divide-y {
:where(& > :not(:last-child)) {
--tw-divide-y-reverse: 0;
border-bottom-style: var(--tw-border-style);
border-top-style: var(--tw-border-style);
border-top-width: calc(1px * var(--tw-divide-y-reverse));
border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
}
}
.divide-gray-200 {
:where(& > :not(:last-child)) {
border-color: var(--color-gray-200);
}
}
.overflow-hidden {
overflow: hidden;
}
.overflow-x-auto {
overflow-x: auto;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-full {
border-radius: calc(infinity * 1px);
}
@@ -371,6 +556,10 @@
.rounded-xl {
border-radius: var(--radius-xl);
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-2 {
border-style: var(--tw-border-style);
border-width: 2px;
@@ -383,15 +572,28 @@
border-style: var(--tw-border-style);
border-width: 4px;
}
.border-b {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px;
}
.border-blue-100 {
border-color: var(--color-blue-100);
}
.border-blue-600 {
border-color: var(--color-blue-600);
}
.border-gray-200 {
border-color: var(--color-gray-200);
}
.border-gray-300 {
border-color: var(--color-gray-300);
}
.border-gray-500 {
border-color: var(--color-gray-500);
}
.border-transparent {
border-color: transparent;
}
.bg-blue-100 {
background-color: var(--color-blue-100);
}
@@ -404,6 +606,12 @@
.bg-blue-600 {
background-color: var(--color-blue-600);
}
.bg-gray-50 {
background-color: var(--color-gray-50);
}
.bg-gray-100 {
background-color: var(--color-gray-100);
}
.bg-gray-200 {
background-color: var(--color-gray-200);
}
@@ -416,9 +624,15 @@
.bg-indigo-500 {
background-color: var(--color-indigo-500);
}
.bg-purple-100 {
background-color: var(--color-purple-100);
}
.bg-purple-500 {
background-color: var(--color-purple-500);
}
.bg-red-100 {
background-color: var(--color-red-100);
}
.bg-red-500 {
background-color: var(--color-red-500);
}
@@ -434,6 +648,9 @@
.object-cover {
object-fit: cover;
}
.p-0 {
padding: calc(var(--spacing) * 0);
}
.p-0\.75 {
padding: calc(var(--spacing) * 0.75);
}
@@ -446,21 +663,60 @@
.p-4 {
padding: calc(var(--spacing) * 4);
}
.p-6 {
padding: calc(var(--spacing) * 6);
}
.p-8 {
padding: calc(var(--spacing) * 8);
}
.px-1 {
padding-inline: calc(var(--spacing) * 1);
}
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
.px-2\.5 {
padding-inline: calc(var(--spacing) * 2.5);
}
.px-3 {
padding-inline: calc(var(--spacing) * 3);
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.py-0\.5 {
padding-block: calc(var(--spacing) * 0.5);
}
.py-1 {
padding-block: calc(var(--spacing) * 1);
}
.py-1\.5 {
padding-block: calc(var(--spacing) * 1.5);
}
.py-2 {
padding-block: calc(var(--spacing) * 2);
}
.py-4 {
padding-block: calc(var(--spacing) * 4);
}
.py-5 {
padding-block: calc(var(--spacing) * 5);
}
.py-8 {
padding-block: calc(var(--spacing) * 8);
}
.py-12 {
padding-block: calc(var(--spacing) * 12);
}
.pb-4 {
padding-bottom: calc(var(--spacing) * 4);
}
.text-center {
text-align: center;
}
.font-mono {
font-family: var(--font-mono);
}
.text-2xl {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
@@ -481,6 +737,10 @@
font-size: var(--text-xl);
line-height: var(--tw-leading, var(--text-xl--line-height));
}
.text-xs {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
.text-\[clamp\(0\.3rem\,5vw\,1rem\)\] {
font-size: clamp(0.3rem, 5vw, 1rem);
}
@@ -512,6 +772,10 @@
--tw-font-weight: var(--font-weight-light);
font-weight: var(--font-weight-light);
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.font-semibold {
--tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold);
@@ -519,15 +783,54 @@
.break-words {
overflow-wrap: break-word;
}
.break-all {
word-break: break-all;
}
.whitespace-pre-wrap {
white-space: pre-wrap;
}
.text-blue-600 {
color: var(--color-blue-600);
}
.text-blue-800 {
color: var(--color-blue-800);
}
.text-gray-400 {
color: var(--color-gray-400);
}
.text-gray-500 {
color: var(--color-gray-500);
}
.text-gray-600 {
color: var(--color-gray-600);
}
.text-gray-700 {
color: var(--color-gray-700);
}
.text-gray-900 {
color: var(--color-gray-900);
}
.text-green-500 {
color: var(--color-green-500);
}
.text-green-600 {
color: var(--color-green-600);
}
.text-green-700 {
color: var(--color-green-700);
}
.text-purple-800 {
color: var(--color-purple-800);
}
.text-red-500 {
color: var(--color-red-500);
}
.text-red-600 {
color: var(--color-red-600);
}
.text-red-700 {
color: var(--color-red-700);
}
.text-stone-900 {
color: var(--color-stone-900);
}
@@ -537,6 +840,13 @@
.italic {
font-style: italic;
}
.underline {
text-decoration-line: underline;
}
.shadow {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-\[0_2px_2px_rgba\(0\,0\,0\,0\.1\)\] {
--tw-shadow: 0 2px 2px var(--tw-shadow-color, rgba(0,0,0,0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -545,14 +855,31 @@
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-sm {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.backdrop-filter {
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.transition-colors {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-200 {
--tw-duration: 200ms;
transition-duration: 200ms;
@@ -577,6 +904,83 @@
}
}
}
.hover\:bg-blue-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-blue-600);
}
}
}
.hover\:bg-blue-700 {
&:hover {
@media (hover: hover) {
background-color: var(--color-blue-700);
}
}
}
.hover\:bg-gray-50 {
&:hover {
@media (hover: hover) {
background-color: var(--color-gray-50);
}
}
}
.hover\:bg-gray-200 {
&:hover {
@media (hover: hover) {
background-color: var(--color-gray-200);
}
}
}
.hover\:bg-gray-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-gray-600);
}
}
}
.hover\:bg-green-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-green-600);
}
}
}
.hover\:bg-red-200 {
&:hover {
@media (hover: hover) {
background-color: var(--color-red-200);
}
}
}
.hover\:bg-red-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-red-600);
}
}
}
.hover\:bg-red-700 {
&:hover {
@media (hover: hover) {
background-color: var(--color-red-700);
}
}
}
.hover\:text-gray-700 {
&:hover {
@media (hover: hover) {
color: var(--color-gray-700);
}
}
}
.hover\:text-red-800 {
&:hover {
@media (hover: hover) {
color: var(--color-red-800);
}
}
}
.focus\:scale-105 {
&:focus {
--tw-scale-x: 105%;
@@ -585,11 +989,49 @@
scale: var(--tw-scale-x) var(--tw-scale-y);
}
}
.focus\:border-blue-500 {
&:focus {
border-color: var(--color-blue-500);
}
}
.focus\:ring-2 {
&:focus {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
.focus\:ring-blue-500 {
&:focus {
--tw-ring-color: var(--color-blue-500);
}
}
.focus\:ring-red-500 {
&:focus {
--tw-ring-color: var(--color-red-500);
}
}
.focus\:ring-offset-2 {
&:focus {
--tw-ring-offset-width: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
}
.focus\:outline-none {
&:focus {
--tw-outline-style: none;
outline-style: none;
}
}
.sm\:grid-cols-3 {
@media (width >= 40rem) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.sm\:rounded-lg {
@media (width >= 40rem) {
border-radius: var(--radius-lg);
}
}
.sm\:px-6 {
@media (width >= 40rem) {
padding-inline: calc(var(--spacing) * 6);
@@ -601,6 +1043,12 @@
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
}
.sm\:text-sm {
@media (width >= 40rem) {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
}
.md\:mb-8 {
@media (width >= 48rem) {
margin-bottom: calc(var(--spacing) * 8);
@@ -862,6 +1310,31 @@ a.qp-a-button {
scale: var(--tw-scale-x) var(--tw-scale-y);
}
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
initial-value: rotateX(0);
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
initial-value: rotateY(0);
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
initial-value: rotateZ(0);
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
initial-value: skewX(0);
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
initial-value: skewY(0);
}
@property --tw-space-y-reverse {
syntax: "*";
inherits: false;
@@ -872,6 +1345,11 @@ a.qp-a-button {
inherits: false;
initial-value: 0;
}
@property --tw-divide-y-reverse {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-border-style {
syntax: "*";
inherits: false;
@@ -936,6 +1414,11 @@ a.qp-a-button {
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur {
syntax: "*";
inherits: false;
@@ -976,6 +1459,42 @@ a.qp-a-button {
syntax: "*";
inherits: false;
}
@property --tw-backdrop-blur {
syntax: "*";
inherits: false;
}
@property --tw-backdrop-brightness {
syntax: "*";
inherits: false;
}
@property --tw-backdrop-contrast {
syntax: "*";
inherits: false;
}
@property --tw-backdrop-grayscale {
syntax: "*";
inherits: false;
}
@property --tw-backdrop-hue-rotate {
syntax: "*";
inherits: false;
}
@property --tw-backdrop-invert {
syntax: "*";
inherits: false;
}
@property --tw-backdrop-opacity {
syntax: "*";
inherits: false;
}
@property --tw-backdrop-saturate {
syntax: "*";
inherits: false;
}
@property --tw-backdrop-sepia {
syntax: "*";
inherits: false;
}
@property --tw-duration {
syntax: "*";
inherits: false;

View File

@@ -13,5 +13,6 @@
{% block content%}{% endblock %}
</div>
{% include 'homepage/partials/_footer.html' %}
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -1,11 +1,39 @@
{% extends 'library/base.html' %}
{% block content %}
<h1>Löschen</h1>
<p>Soll das Quiz "{{ object.name }}" wirklich gelöscht werden?</p>
<form method="post">
<div class="container mx-auto px-4">
<div class="max-w-lg mx-auto mt-10">
<div class="bg-white rounded-lg shadow-md p-6">
<div class="text-center">
<svg class="mx-auto h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<h2 class="mt-4 text-xl font-bold text-gray-900">Löschen bestätigen</h2>
<p class="mt-2 text-sm text-gray-600">
{% if object.quiz_id %}
Möchten Sie die Frage "{{ object.data.question }}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.
{% else %}
Möchten Sie das Quiz "{{ object.name }}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.
{% endif %}
</p>
</div>
<form method="post" class="mt-6">
{% csrf_token %}
<button type="submit">Löschen</button>
<a href="{% url 'library:overview_quiz' %}">Abbrechen</a>
</form>
<div class="flex justify-center space-x-3">
<a href="{% if object.quiz_id %}{% url 'library:detail_quiz' object.quiz_id.id %}{% else %}{% url 'library:overview_quiz' %}{% endif %}"
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Abbrechen
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Löschen
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,19 +1,99 @@
{% extends 'library/base.html' %}
{% block content %}
<h1>{{ quiz.name }}</h1>
<p>{{ quiz.description }}</p>
<a href="{% url 'library:edit_quiz' quiz.id %}">Bearbeiten</a>
<a href="{% url 'library:delete_quiz' quiz.id %}">Löschen</a>
<div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">{{ quiz.name }}</h1>
<div class="flex space-x-2">
<a href="{% url 'library:edit_quiz' quiz.id %}" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-colors">Bearbeiten</a>
<a href="{% url 'library:delete_quiz' quiz.id %}" class="bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 transition-colors">Löschen</a>
</div>
</div>
<h2>Fragen</h2>
<ul>
<a href="{% url 'library:new_question' %}">Neue Frage erstellen</a>
{% if quiz.description %}
<p class="text-gray-600 mb-8">{{ quiz.description }}</p>
{% endif %}
<div class="bg-white rounded-lg shadow-md">
<div class="border-b border-gray-200 p-4">
<div class="flex justify-between items-center">
<h2 class="text-xl font-semibold">Fragen</h2>
<div class="flex space-x-2">
<a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}"
class="bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600 transition-colors">
Multiple Choice
</a>
<a href="{% url 'library:new_question' %}?type=true_false&quiz_id={{ quiz.id }}"
class="bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600 transition-colors">
Wahr/Falsch
</a>
</div>
</div>
</div>
{% if questions %}
<div class="divide-y divide-gray-200">
{% for question in questions %}
<li>{{ question.data }}</li>
<a href="{% url 'library:edit_question' question.id %}">Bearbeiten</a>
<a href="{% url 'library:delete_question' question.id %}">Löschen</a>
<div class="p-4 hover:bg-gray-50">
<div class="flex justify-between items-start">
<div class="flex-grow">
<p class="font-medium">{{ question.data.question }}</p>
<div class="mt-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800{% else %}bg-purple-100 text-purple-800{% endif %}">
{% if question.data.type == 'multiple_choice' %}Multiple Choice{% else %}Wahr/Falsch{% endif %}
</span>
</div>
<div class="mt-2">
{% if question.data.type == 'multiple_choice' %}
<ul class="space-y-1">
{% for option in question.data.options %}
<li class="flex items-center text-sm">
{% if option.is_correct %}
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="font-medium text-green-700">{{ option.value }}</span>
{% else %}
<svg class="h-4 w-4 text-gray-400 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/>
</svg>
<span class="text-gray-600">{{ option.value }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
</ul>
{% else %}
<p class="text-sm">
<span class="text-gray-500">Richtige Antwort:</span>
<span class="ml-1 font-medium {% if question.data.correct_answer %}text-green-700{% else %}text-red-700{% endif %}">
{% if question.data.correct_answer %}Wahr{% else %}Falsch{% endif %}
</span>
</p>
{% endif %}
</div>
</div>
<div class="flex space-x-2 ml-4">
<a href="{% url 'library:edit_question' question.id %}"
class="bg-gray-100 text-gray-700 px-3 py-1 rounded hover:bg-gray-200 transition-colors">
Bearbeiten
</a>
<a href="{% url 'library:delete_question' question.id %}"
class="bg-red-100 text-red-700 px-3 py-1 rounded hover:bg-red-200 transition-colors">
Löschen
</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="p-8 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
<p class="mt-2 text-sm text-gray-500">Noch keine Fragen vorhanden. Erstellen Sie eine neue Frage, um loszulegen!</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,164 @@
{% extends 'library/base.html' %}
{% block content %}
<div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">{% if question %}Frage bearbeiten{% else %}Neue Multiple Choice Frage{% endif %}</h1>
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
Zurück zum Quiz
</a>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<form method="post" class="space-y-6">
{% csrf_token %}
<!-- Question Text -->
<div>
<label for="question" class="block text-sm font-medium text-gray-700">Frage</label>
<div class="mt-1">
<input type="text" name="question" id="question"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
value="{{ question.data.question|default:'' }}" required>
</div>
</div>
<!-- Options -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Antwortmöglichkeiten</label>
<div id="options-container" class="space-y-3">
{% if question and question.data.options %}
{% for option in question.data.options %}
<div class="flex items-center space-x-3">
<input type="text" name="option_{{ forloop.counter }}"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
value="{{ option.value }}" required>
<label class="inline-flex items-center">
<input type="checkbox" name="correct_{{ forloop.counter }}" value="1"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
{% if option.is_correct %}checked{% endif %}>
<span class="ml-2 text-sm text-gray-600">Richtig</span>
</label>
<button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
{% endfor %}
{% else %}
<div class="flex items-center space-x-3">
<input type="text" name="option_1"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required>
<label class="inline-flex items-center">
<input type="checkbox" name="correct_1" value="1"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
<span class="ml-2 text-sm text-gray-600">Richtig</span>
</label>
<button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
<div class="flex items-center space-x-3">
<input type="text" name="option_2"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required>
<label class="inline-flex items-center">
<input type="checkbox" name="correct_2" value="1"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
<span class="ml-2 text-sm text-gray-600">Richtig</span>
</label>
<button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
{% endif %}
</div>
<button type="button" onclick="addOption()"
class="mt-3 inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="h-4 w-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Option hinzufügen
</button>
</div>
<div class="flex justify-end space-x-3">
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Abbrechen
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{% if question %}Speichern{% else %}Erstellen{% endif %}
</button>
</div>
</form>
</div>
</div>
<script>
function addOption() {
const container = document.getElementById('options-container');
const optionCount = container.children.length + 1;
if (optionCount > 6) {
alert('Maximal 6 Optionen sind erlaubt.');
return;
}
const div = document.createElement('div');
div.className = 'flex items-center space-x-3';
div.innerHTML = `
<input type="text" name="option_${optionCount}"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
required>
<label class="inline-flex items-center">
<input type="checkbox" name="correct_${optionCount}" value="1"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
<span class="ml-2 text-sm text-gray-600">Richtig</span>
</label>
<button type="button" onclick="removeOption(this)"
class="text-red-600 hover:text-red-800 transition-colors">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
`;
container.appendChild(div);
}
function removeOption(button) {
const container = document.getElementById('options-container');
if (container.children.length > 2) {
button.parentElement.remove();
updateOptionNumbers();
} else {
alert('Mindestens zwei Optionen müssen vorhanden sein.');
}
}
function updateOptionNumbers() {
const container = document.getElementById('options-container');
const options = container.children;
Array.from(options).forEach((option, index) => {
const number = index + 1;
const textInput = option.querySelector('input[type="text"]');
const checkbox = option.querySelector('input[type="checkbox"]');
textInput.name = `option_${number}`;
checkbox.name = `correct_${number}`;
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,59 @@
{% extends 'library/base.html' %}
{% block content %}
<div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">{% if question %}Frage bearbeiten{% else %}Neue Wahr/Falsch Frage{% endif %}</h1>
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
Zurück zum Quiz
</a>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<form method="post" class="space-y-6">
{% csrf_token %}
<!-- Question Text -->
<div>
<label for="question" class="block text-sm font-medium text-gray-700">Frage</label>
<div class="mt-1">
<input type="text" name="question" id="question"
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
value="{{ question.question|default:'' }}" required>
</div>
</div>
<!-- True/False Selection -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Richtige Antwort</label>
<div class="flex space-x-4">
<label class="inline-flex items-center">
<input type="radio" name="correct_answer" value="true"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300"
{% if question.correct_answer %}checked{% endif %}>
<span class="ml-2 text-sm text-gray-600">Wahr</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="correct_answer" value="false"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300"
{% if not question.correct_answer %}checked{% endif %}>
<span class="ml-2 text-sm text-gray-600">Falsch</span>
</label>
</div>
</div>
<div class="flex justify-end space-x-3">
<a href="{% url 'library:detail_quiz' quiz.id %}"
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Abbrechen
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{% if question %}Speichern{% else %}Erstellen{% endif %}
</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -1,8 +1,11 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.urls import reverse
from django.contrib import messages
from .models import QivipQuiz, QivipQuestion
from .forms import QuizForm, QuestionForm
import json
# Übersicht aller Quizze
@login_required
@@ -52,47 +55,202 @@ def delete_quiz(request, pk):
def detail_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
questions = QivipQuestion.objects.filter(quiz_id=quiz)
return render(request, 'library/detail_quiz.html', {'quiz': quiz, 'questions': questions})
# Parse JSON data for each question
for question in questions:
if question.data:
question.data = json.loads(question.data)
context = {
'quiz': quiz,
'questions': questions
}
return render(request, 'library/detail_quiz.html', context)
# Übersicht aller Fragen
@login_required
def question_list(request):
questions = QivipQuestion.objects.filter(quiz_id__user_id=request.user)
# Parse JSON data for each question
for question in questions:
if question.data:
question.data = json.loads(question.data)
return render(request, 'library/question_list.html', {'questions': questions})
# Neue Frage erstellen
@login_required
def new_question(request):
question_type = request.GET.get('type')
quiz_id = request.GET.get('quiz_id')
if not quiz_id:
messages.error(request, 'Quiz ID muss angegeben werden.')
return redirect('library:overview_quiz')
try:
quiz = QivipQuiz.objects.get(pk=quiz_id, user_id=request.user)
except QivipQuiz.DoesNotExist:
messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.')
return redirect('library:overview_quiz')
if question_type not in ['multiple_choice', 'true_false']:
base_url = reverse('library:new_question')
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
if request.method == 'POST':
form = QuestionForm(request.POST)
if form.is_valid():
question = form.save(commit=False)
# Prüfe ob der User Zugriff auf das Quiz hat
if question.quiz_id.user_id != request.user:
return redirect('library:question_list')
question_text = request.POST.get('question')
# Handle different question types
if question_type == 'true_false':
correct_answer = request.POST.get('correct_answer') == 'true'
json_data = {
'type': question_type,
'question': question_text,
'options': [
{'value': 'Wahr', 'is_correct': correct_answer},
{'value': 'Falsch', 'is_correct': not correct_answer}
]
}
elif question_type == 'multiple_choice':
options = []
i = 1
# Limit to maximum 6 options
while request.POST.get(f'option_{i}') and i <= 6:
is_correct = request.POST.get(f'correct_{i}') == '1'
options.append({
'order': i,
'value': request.POST.get(f'option_{i}'),
'is_correct': is_correct
})
i += 1
# Validate minimum 2 options
if len(options) < 2:
messages.error(request, 'Mindestens zwei Optionen müssen angegeben werden.')
return redirect(request.get_full_path())
json_data = {
'type': question_type,
'question': question_text,
'options': options
}
question = QivipQuestion()
question.data = json.dumps(json_data)
question.quiz_id = quiz
question.type = question_type
question.question = question_text
question.save()
return redirect('library:edit_question', pk=question.pk)
return redirect('library:detail_quiz', pk=quiz.pk)
else:
form = QuestionForm()
return render(request, 'library/form.html', {'form': form})
# Initialize empty question data for new questions
question_data = {
'type': question_type,
'question': '',
'options': []
}
if question_type == 'true_false':
question_data['options'] = [
{'value': 'Wahr', 'is_correct': True},
{'value': 'Falsch', 'is_correct': False}
]
elif question_type == 'multiple_choice':
question_data['options'] = [
{'value': '', 'is_correct': False},
{'value': '', 'is_correct': False}
]
template_name = f'library/question/question_{question_type}.html'
context = {
'question': question_data,
'quiz_id': quiz_id,
'quiz': quiz
}
return render(request, template_name, context)
# Frage bearbeiten
@login_required
def edit_question(request, pk):
question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user)
if question.quiz_id.user_id != request.user:
return redirect('library:question_list')
# Parse the existing JSON data
question_data = json.loads(question.data) if question.data else {}
question_type = question_data.get('type', 'multiple_choice')
if request.method == 'POST':
form = QuestionForm(request.POST, instance=question)
if form.is_valid():
form.save()
return redirect('library:edit_question', pk=pk)
question_text = request.POST.get('question')
# Handle different question types
if question_type == 'true_false':
correct_answer = request.POST.get('correct_answer') == 'true'
json_data = {
'type': question_type,
'question': question_text,
'correct_answer': correct_answer,
'options': [
{'value': 'Wahr', 'is_correct': correct_answer},
{'value': 'Falsch', 'is_correct': not correct_answer}
]
}
elif question_type == 'multiple_choice':
options = []
i = 1
# Limit to maximum 6 options
while request.POST.get(f'option_{i}') and i <= 6:
options.append({
'order': i,
'value': request.POST.get(f'option_{i}'),
'is_correct': int(request.POST.get(f'correct_{i}', 0))
})
i += 1
# Validate minimum 2 options
if len(options) < 2:
messages.error(request, 'Mindestens zwei Optionen müssen angegeben werden.')
return redirect(request.get_full_path())
# Validate maximum 6 options
if len(options) > 6:
messages.error(request, 'Maximal 6 Optionen sind erlaubt.')
return redirect(request.get_full_path())
json_data = {
'type': question_type,
'question': question_text,
'options': options
}
question.question = question_text
question.type = question_type
question.data = json.dumps(json_data)
question.save()
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
else:
form = QuestionForm(instance=question)
return render(request, 'library/form.html', {'form': form})
question_data = json.loads(question.data) if question.data else {}
template_name = f'library/question/question_{question_type}.html'
context = {
'question': question_data,
'quiz_id': question.quiz_id.pk,
'quiz': question.quiz_id
}
return render(request, template_name, context)
# Frage löschen
@login_required
def delete_question(request, pk):
question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user)
# Parse JSON data for question
if question.data:
question.data = json.loads(question.data)
if request.method == 'POST':
question.delete()
return redirect('library:detail_quiz', pk=question.quiz_id.pk)