Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 149 additions & 138 deletions client/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"react-hook-form": "^7.68.0",
"react-oidc-context": "^3.3.0",
"react-router-dom": "^7.10.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"zod": "^4.1.13"
Expand Down
2 changes: 2 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { ProtectedRoute } from "@/components/layout/ProtectedRoute";
import { AuthProvider } from "@/components/providers/AuthProvider";
import { Toaster } from "@/components/ui/sonner";
import { AboutPage } from "@/pages/AboutPage";
import { ArtemisRequestPage } from "@/pages/ArtemisRequestPage";
import { ExternalLinksAdminPage } from "@/pages/ExternalLinksAdminPage";
Expand Down Expand Up @@ -58,6 +59,7 @@ function App() {
<Route path="/privacy" element={<PrivacyPage />} />
<Route path="/imprint" element={<ImprintPage />} />
</Routes>
<Toaster position="top-center" richColors closeButton />
</AuthProvider>
</BrowserRouter>
);
Expand Down
3 changes: 3 additions & 0 deletions client/src/components/artemis-request/ArtemisRequestForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ import { ReviewStep } from "./steps/ReviewStep";
interface ArtemisRequestFormProps {
onSubmit: (data: ArtemisRequest, githubUser?: GitHubUser) => Promise<void>;
isSubmitting: boolean;
submitFailed?: boolean;
}

export function ArtemisRequestForm({
onSubmit,
isSubmitting,
submitFailed,
}: ArtemisRequestFormProps) {
const { isAuthenticated, user } = useAuth();
const [currentStep, setCurrentStep] = useState(1);
Expand Down Expand Up @@ -217,6 +219,7 @@ export function ArtemisRequestForm({
currentStep={currentStep}
totalSteps={steps.length}
isSubmitting={isSubmitting}
submitFailed={submitFailed}
isNextDisabled={isNextDisabled()}
onPrevious={handlePrevious}
onNext={handleNext}
Expand Down
39 changes: 0 additions & 39 deletions client/src/components/shared/RequestErrorCard.tsx

This file was deleted.

24 changes: 22 additions & 2 deletions client/src/components/support-request/SupportRequestForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CircleX, Loader2 } from "lucide-react";
import { FormProvider, useForm } from "react-hook-form";
import { RequesterInfo } from "@/components/shared/RequesterInfo";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -34,11 +35,13 @@ import {
interface SupportRequestFormProps {
onSubmit: (data: SupportRequest) => void;
isSubmitting: boolean;
submitFailed?: boolean;
}

export function SupportRequestForm({
onSubmit,
isSubmitting,
submitFailed,
}: SupportRequestFormProps) {
const { isAuthenticated, user } = useAuth();

Expand Down Expand Up @@ -211,8 +214,25 @@ export function SupportRequestForm({
</Card>

<div className="flex justify-end">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit Request"}
<Button
type="submit"
disabled={isSubmitting}
variant={submitFailed ? "destructive" : "default"}
className={submitFailed ? "animate-shake" : ""}
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Submitting...
</>
) : submitFailed ? (
<>
<CircleX className="mr-2 h-4 w-4" />
Retry Submission
</>
) : (
"Submit Request"
)}
</Button>
</div>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import { ReviewStep } from "./steps/ReviewStep";
interface TUMGuestRequestFormProps {
onSubmit: (data: TUMGuestRequest) => Promise<void>;
isSubmitting: boolean;
submitFailed?: boolean;
}

export function TUMGuestRequestForm({
onSubmit,
isSubmitting,
submitFailed,
}: TUMGuestRequestFormProps) {
const { isAuthenticated, login } = useAuth();
const [currentStep, setCurrentStep] = useState(1);
Expand Down Expand Up @@ -208,6 +210,7 @@ export function TUMGuestRequestForm({
currentStep={currentStep}
totalSteps={steps.length}
isSubmitting={isSubmitting}
submitFailed={submitFailed}
isNextDisabled={isNextDisabled}
onPrevious={handlePrevious}
onNext={handleNext}
Expand Down
18 changes: 16 additions & 2 deletions client/src/components/ui/form-navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ArrowLeft, ArrowRight, Loader2 } from "lucide-react";
import { ArrowLeft, ArrowRight, CircleX, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

interface FormNavigationProps {
currentStep: number;
totalSteps: number;
isSubmitting: boolean;
submitFailed?: boolean;
isNextDisabled?: boolean;
onPrevious: () => void;
onNext: () => void;
Expand All @@ -15,6 +17,7 @@ export function FormNavigation({
currentStep,
totalSteps,
isSubmitting,
submitFailed = false,
isNextDisabled = false,
onPrevious,
onNext,
Expand All @@ -35,12 +38,23 @@ export function FormNavigation({
</Button>

{isLastStep ? (
<Button type="button" onClick={onSubmit} disabled={isSubmitting}>
<Button
type="button"
onClick={onSubmit}
disabled={isSubmitting}
variant={submitFailed ? "destructive" : "default"}
className={cn(submitFailed && "animate-shake")}
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Submitting...
</>
) : submitFailed ? (
<>
<CircleX className="mr-2 h-4 w-4" />
Retry Submission
</>
) : (
"Submit Request"
)}
Expand Down
41 changes: 41 additions & 0 deletions client/src/components/ui/sonner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react";
import type { CSSProperties } from "react";
import { Toaster as Sonner, type ToasterProps } from "sonner";

const Toaster = ({ ...props }: ToasterProps) => {
Comment thread
magkue marked this conversation as resolved.
return (
<Sonner
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
toastOptions={{
classNames: {
actionButton:
"!bg-background !text-foreground !font-medium !text-sm !px-3 !py-1.5 !h-auto !rounded-md !border !border-foreground/15 hover:!bg-accent",
},
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as CSSProperties
}
Comment thread
magkue marked this conversation as resolved.
{...props}
/>
);
};

export { Toaster };
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import { ReviewStep } from "./steps/ReviewStep";
interface VMAccessRequestFormProps {
onSubmit: (data: VMAccessRequest) => Promise<void>;
isSubmitting: boolean;
submitFailed?: boolean;
}

export function VMAccessRequestForm({
onSubmit,
isSubmitting,
submitFailed,
}: VMAccessRequestFormProps) {
const [currentStep, setCurrentStep] = useState(1);

Expand Down Expand Up @@ -124,6 +126,7 @@ export function VMAccessRequestForm({
currentStep={currentStep}
totalSteps={VM_ACCESS_STEPS.length}
isSubmitting={isSubmitting}
submitFailed={submitFailed}
isNextDisabled={hasErrorsInCurrentStep()}
onPrevious={handlePrevious}
onNext={handleNext}
Expand Down
8 changes: 7 additions & 1 deletion client/src/components/vm-request/VMRequestForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { UsersStep } from "./steps/UsersStep";
interface VMRequestFormProps {
onSubmit: (data: VMRequest) => Promise<void>;
isSubmitting: boolean;
submitFailed?: boolean;
}

const stepSchemas = [
Expand All @@ -35,7 +36,11 @@ const stepSchemas = [
null, // Review step - full validation
];

export function VMRequestForm({ onSubmit, isSubmitting }: VMRequestFormProps) {
export function VMRequestForm({
onSubmit,
isSubmitting,
submitFailed,
}: VMRequestFormProps) {
const [currentStep, setCurrentStep] = useState(1);

const form = useForm<VMRequest>({
Expand Down Expand Up @@ -163,6 +168,7 @@ export function VMRequestForm({ onSubmit, isSubmitting }: VMRequestFormProps) {
currentStep={currentStep}
totalSteps={VM_REQUEST_STEPS.length}
isSubmitting={isSubmitting}
submitFailed={submitFailed}
isNextDisabled={hasErrorsInCurrentStep()}
onPrevious={handlePrevious}
onNext={handleNext}
Expand Down
29 changes: 29 additions & 0 deletions client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,32 @@
@apply bg-background text-foreground;
}
}

@keyframes shake {
0%,
100% {
transform: translateX(0);
}
20% {
transform: translateX(-4px);
}
40% {
transform: translateX(4px);
}
60% {
transform: translateX(-3px);
}
80% {
transform: translateX(3px);
}
}

.animate-shake {
animation: shake 0.4s ease-in-out;
}
Comment thread
magkue marked this conversation as resolved.

@media (prefers-reduced-motion: reduce) {
.animate-shake {
animation: none;
}
}
Loading
Loading