Compare commits
3 commits
c6d0bb8dca
...
e4d61fba24
| Author | SHA1 | Date | |
|---|---|---|---|
| e4d61fba24 | |||
| 6974f4fc44 | |||
| d14caa5aba |
13 changed files with 859 additions and 58 deletions
|
|
@ -0,0 +1,29 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Registration.API.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class AdminController(IConfiguration configuration) : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration = configuration;
|
||||||
|
|
||||||
|
[HttpPost("login")]
|
||||||
|
public IActionResult Login([FromBody] LoginRequest request)
|
||||||
|
{
|
||||||
|
var adminPassword = _configuration["Admin:Password"] ?? "admin123";
|
||||||
|
if (request.Username == "admin" && request.Password == adminPassword)
|
||||||
|
{
|
||||||
|
// In a real app, we would return a JWT token here
|
||||||
|
return Ok(new { Success = true, Token = "fake-jwt-token" });
|
||||||
|
}
|
||||||
|
return Unauthorized(new { Success = false, Message = "Invalid credentials" });
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Registration.Domain.Models;
|
||||||
|
using Registration.Infra.Repositories;
|
||||||
|
|
||||||
|
namespace Registration.API.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class ContentController(IMemberRepository memberRepository) : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMemberRepository _memberRepository = memberRepository;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetContent()
|
||||||
|
{
|
||||||
|
var content = await _memberRepository.GetEventContent();
|
||||||
|
return Ok(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> UpdateContent([FromBody] EventContent content)
|
||||||
|
{
|
||||||
|
await _memberRepository.UpdateEventContent(content);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,20 @@ namespace Registration.API.Controllers
|
||||||
{
|
{
|
||||||
private readonly IMemberRepository _memberRepository = memberRepository;
|
private readonly IMemberRepository _memberRepository = memberRepository;
|
||||||
|
|
||||||
|
[HttpGet("register/{ssn}")]
|
||||||
|
public IActionResult ValidateSsn(string ssn)
|
||||||
|
{
|
||||||
|
// Should talk to the auth api to validate the ssn properly.
|
||||||
|
if (ssn.Length == 10 && long.TryParse(ssn, out _))
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("register/{ssn}")]
|
[HttpPost("register/{ssn}")]
|
||||||
public async Task<IActionResult> RegisterMember(string ssn)
|
public async Task<IActionResult> RegisterMember(string ssn)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,4 @@ public class WeatherForecast
|
||||||
|
|
||||||
public string? Summary { get; set; }
|
public string? Summary { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
"Security": {
|
"Security": {
|
||||||
"SsnPepper": "VBYTES_LAN_2026_SECRET_PEPPER"
|
"SsnPepper": "VBYTES_LAN_2026_SECRET_PEPPER"
|
||||||
},
|
},
|
||||||
|
"Admin": {
|
||||||
|
"Password": "admin"
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|
|
||||||
13
src/Registration/Registration.Domain/Models/EventContent.cs
Normal file
13
src/Registration/Registration.Domain/Models/EventContent.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Registration.Domain.Models;
|
||||||
|
|
||||||
|
public class EventContent
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string SubTitle { get; set; } = string.Empty;
|
||||||
|
public string EventDate { get; set; } = string.Empty;
|
||||||
|
public string EventTime { get; set; } = string.Empty;
|
||||||
|
public string LocationName { get; set; } = string.Empty;
|
||||||
|
public string LocationAddress { get; set; } = string.Empty;
|
||||||
|
public string WhatToBring { get; set; } = string.Empty;
|
||||||
|
public string RulesAndGdpr { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ namespace Registration.Domain.Models;
|
||||||
|
|
||||||
public class Participant
|
public class Participant
|
||||||
{
|
{
|
||||||
|
public required bool IsMember { get; set; }
|
||||||
public required string FirstName { get; set; }
|
public required string FirstName { get; set; }
|
||||||
public required string SurName { get; set; }
|
public required string SurName { get; set; }
|
||||||
public required string Grade { get; set; }
|
public required string Grade { get; set; }
|
||||||
|
|
@ -15,5 +16,3 @@ public class Participant
|
||||||
public string? Friends { get; set; }
|
public string? Friends { get; set; }
|
||||||
public string? SpecialDiet { get; set; }
|
public string? SpecialDiet { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
|
using Registration.Domain.Models;
|
||||||
|
|
||||||
namespace Registration.Infra.Repositories;
|
namespace Registration.Infra.Repositories;
|
||||||
|
|
||||||
|
|
@ -12,6 +13,8 @@ public interface IMemberRepository
|
||||||
Task<bool> GetIsRegistered(string ssn);
|
Task<bool> GetIsRegistered(string ssn);
|
||||||
Task<bool> AddRegistration(string ssn);
|
Task<bool> AddRegistration(string ssn);
|
||||||
Task ClearRegistrations();
|
Task ClearRegistrations();
|
||||||
|
Task<EventContent> GetEventContent();
|
||||||
|
Task UpdateEventContent(EventContent content);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MemberRepository(IConfiguration configuration) : IMemberRepository
|
public class MemberRepository(IConfiguration configuration) : IMemberRepository
|
||||||
|
|
@ -41,6 +44,22 @@ public class MemberRepository(IConfiguration configuration) : IMemberRepository
|
||||||
ssn_index VARCHAR(255) NOT NULL UNIQUE
|
ssn_index VARCHAR(255) NOT NULL UNIQUE
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_members_ssn_index ON members(ssn_index);
|
CREATE INDEX IF NOT EXISTS idx_members_ssn_index ON members(ssn_index);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS event_content (
|
||||||
|
id INT PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
sub_title TEXT,
|
||||||
|
event_date TEXT,
|
||||||
|
event_time TEXT,
|
||||||
|
location_name TEXT,
|
||||||
|
location_address TEXT,
|
||||||
|
what_to_bring TEXT,
|
||||||
|
rules_and_gdpr TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO event_content (id, title, sub_title, event_date, event_time, location_name, location_address, what_to_bring, rules_and_gdpr)
|
||||||
|
SELECT 1, '', '', '', '', '', '', '', ''
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM event_content WHERE id = 1);
|
||||||
";
|
";
|
||||||
|
|
||||||
await connection.ExecuteAsync(sql);
|
await connection.ExecuteAsync(sql);
|
||||||
|
|
@ -75,5 +94,29 @@ public class MemberRepository(IConfiguration configuration) : IMemberRepository
|
||||||
var sql = "DELETE FROM members";
|
var sql = "DELETE FROM members";
|
||||||
await connection.ExecuteAsync(sql);
|
await connection.ExecuteAsync(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<EventContent> GetEventContent()
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
var content = await connection.QueryFirstOrDefaultAsync<EventContent>("SELECT title, sub_title as SubTitle, event_date as EventDate, event_time as EventTime, location_name as LocationName, location_address as LocationAddress, what_to_bring as WhatToBring, rules_and_gdpr as RulesAndGdpr FROM event_content WHERE id = 1");
|
||||||
|
return content ?? new EventContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateEventContent(EventContent content)
|
||||||
|
{
|
||||||
|
using var connection = CreateConnection();
|
||||||
|
var sql = @"
|
||||||
|
UPDATE event_content SET
|
||||||
|
title = @Title,
|
||||||
|
sub_title = @SubTitle,
|
||||||
|
event_date = @EventDate,
|
||||||
|
event_time = @EventTime,
|
||||||
|
location_name = @LocationName,
|
||||||
|
location_address = @LocationAddress,
|
||||||
|
what_to_bring = @WhatToBring,
|
||||||
|
rules_and_gdpr = @RulesAndGdpr
|
||||||
|
WHERE id = 1";
|
||||||
|
await connection.ExecuteAsync(sql, content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
201
src/Web/lan-frontend/app/admin/dashboard/page.tsx
Normal file
201
src/Web/lan-frontend/app/admin/dashboard/page.tsx
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface EventContent {
|
||||||
|
title: string;
|
||||||
|
subTitle: string;
|
||||||
|
eventDate: string;
|
||||||
|
eventTime: string;
|
||||||
|
locationName: string;
|
||||||
|
locationAddress: string;
|
||||||
|
whatToBring: string;
|
||||||
|
rulesAndGdpr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminDashboard() {
|
||||||
|
const [content, setContent] = useState<EventContent | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [message, setMessage] = useState({ type: "", text: "" });
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem("adminToken");
|
||||||
|
if (!token) {
|
||||||
|
router.push("/admin/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch("/api/Content")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
setContent(data);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to fetch content", err);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem("adminToken");
|
||||||
|
router.push("/admin/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!content) return;
|
||||||
|
setSaving(true);
|
||||||
|
setMessage({ type: "", text: "" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/Content", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(content),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setMessage({ type: "success", text: "Content saved successfully!" });
|
||||||
|
} else {
|
||||||
|
setMessage({ type: "error", text: "Failed to save content." });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setMessage({ type: "error", text: "An error occurred while saving." });
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setContent((prev) => prev ? { ...prev, [name]: value } : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <div className="p-8">Loading...</div>;
|
||||||
|
if (!content) return <div className="p-8 text-red-500">Error loading content.</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-8 font-sans">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
|
||||||
|
<div className="space-x-4">
|
||||||
|
<Link href="/" className="text-blue-600 hover:underline">View Site</Link>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="bg-gray-200 px-4 py-2 rounded-md hover:bg-gray-300"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSave} className="bg-white p-8 rounded-lg shadow-md space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Page Title</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 p-2 text-gray-900"
|
||||||
|
value={content.title}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Sub-title</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="subTitle"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 p-2 text-gray-900"
|
||||||
|
value={content.subTitle}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Event Date</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="eventDate"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 p-2 text-gray-900"
|
||||||
|
value={content.eventDate}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Event Time</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="eventTime"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 p-2 text-gray-900"
|
||||||
|
value={content.eventTime}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Location Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="locationName"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 p-2 text-gray-900"
|
||||||
|
value={content.locationName}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Location Address</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="locationAddress"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 p-2 text-gray-900"
|
||||||
|
value={content.locationAddress}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">What to Bring (One item per line)</label>
|
||||||
|
<textarea
|
||||||
|
name="whatToBring"
|
||||||
|
rows={6}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 p-2 font-mono text-sm text-gray-900"
|
||||||
|
value={content.whatToBring}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Rules & GDPR</label>
|
||||||
|
<textarea
|
||||||
|
name="rulesAndGdpr"
|
||||||
|
rows={4}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 p-2 text-gray-900"
|
||||||
|
value={content.rulesAndGdpr}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message.text && (
|
||||||
|
<div className={`p-4 rounded-md ${message.type === "success" ? "bg-green-50 text-green-800" : "bg-red-50 text-red-800"}`}>
|
||||||
|
{message.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="pt-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
className="w-full bg-blue-600 text-white py-3 rounded-md hover:bg-blue-700 disabled:bg-gray-400 font-bold"
|
||||||
|
>
|
||||||
|
{saving ? "Saving Changes..." : "Save Content Changes"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
src/Web/lan-frontend/app/admin/login/page.tsx
Normal file
77
src/Web/lan-frontend/app/admin/login/page.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export default function AdminLogin() {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/Admin/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.token) {
|
||||||
|
localStorage.setItem("adminToken", data.token);
|
||||||
|
router.push("/admin/dashboard");
|
||||||
|
} else {
|
||||||
|
setError(data.message || "Invalid credentials");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError("An error occurred. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||||
|
<div className="bg-white p-8 rounded-lg shadow-md w-96">
|
||||||
|
<h1 className="text-2xl font-bold mb-6 text-center">Admin Login</h1>
|
||||||
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Username</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 p-2 text-gray-900"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 p-2 text-gray-900"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
|
||||||
|
>
|
||||||
|
{loading ? "Logging in..." : "Login"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,65 +1,122 @@
|
||||||
import Image from "next/image";
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface EventContent {
|
||||||
|
title: string;
|
||||||
|
subTitle: string;
|
||||||
|
eventDate: string;
|
||||||
|
eventTime: string;
|
||||||
|
locationName: string;
|
||||||
|
locationAddress: string;
|
||||||
|
whatToBring: string;
|
||||||
|
rulesAndGdpr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LandingPage() {
|
||||||
|
const [content, setContent] = useState<EventContent | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/Content")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => setContent(data))
|
||||||
|
.catch((err) => console.error("Failed to fetch content", err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div className="min-h-screen bg-white text-gray-900 font-sans">
|
||||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
{/* Hero Section */}
|
||||||
<Image
|
<header className="relative h-[70vh] flex items-center justify-center bg-black overflow-hidden">
|
||||||
className="dark:invert"
|
<div className="absolute inset-0 opacity-50">
|
||||||
src="/next.svg"
|
<Image
|
||||||
alt="Next.js logo"
|
src="https://images.unsplash.com/photo-1542751371-adc38448a05e?auto=format&fit=crop&q=80&w=2070"
|
||||||
width={100}
|
alt="LAN Event"
|
||||||
height={20}
|
fill
|
||||||
priority
|
className="object-cover"
|
||||||
/>
|
priority
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
/>
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
</div>
|
||||||
To get started, edit the page.tsx file.
|
<div className="relative z-10 text-center px-4">
|
||||||
|
<h1 className="text-5xl md:text-7xl font-extrabold text-white tracking-tight mb-4">
|
||||||
|
{content.title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
<p className="text-xl md:text-2xl text-blue-400 font-medium">
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
{content.subTitle}
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
</header>
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
{/* Info Section */}
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<main className="max-w-4xl mx-auto py-16 px-6 space-y-20">
|
||||||
target="_blank"
|
{(content.eventDate || content.eventTime || content.locationName || content.locationAddress) && (
|
||||||
rel="noopener noreferrer"
|
<section className="space-y-6">
|
||||||
|
<h2 className="text-3xl font-bold border-b-2 border-blue-500 pb-2 inline-block">Event Information</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mt-6">
|
||||||
|
{(content.eventDate || content.eventTime) && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-semibold text-lg text-blue-600">Date & Time</h3>
|
||||||
|
{content.eventDate && <p>{content.eventDate}</p>}
|
||||||
|
{content.eventTime && <p>{content.eventTime}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(content.locationName || content.locationAddress) && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="font-semibold text-lg text-blue-600">Location</h3>
|
||||||
|
{content.locationName && <p>{content.locationName}</p>}
|
||||||
|
{content.locationAddress && <p>{content.locationAddress}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{content.whatToBring && (
|
||||||
|
<section className="space-y-6">
|
||||||
|
<h2 className="text-3xl font-bold border-b-2 border-blue-500 pb-2 inline-block">What to Bring</h2>
|
||||||
|
<ul className="list-disc list-inside space-y-3 text-lg text-gray-700 whitespace-pre-line">
|
||||||
|
{content.whatToBring}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{content.rulesAndGdpr && (
|
||||||
|
<section className="space-y-6">
|
||||||
|
<h2 className="text-3xl font-bold border-b-2 border-blue-500 pb-2 inline-block">Rules & GDPR</h2>
|
||||||
|
<p className="text-lg text-gray-700 leading-relaxed">
|
||||||
|
{content.rulesAndGdpr}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Call to Action */}
|
||||||
|
<section className="flex flex-col items-center justify-center space-y-8 pt-12 border-t border-gray-100">
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<h2 className="text-4xl font-bold">Ready to join?</h2>
|
||||||
|
<p className="text-gray-600 text-lg">Seats are limited, so secure your spot today!</p>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/register"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white text-xl font-bold py-4 px-12 rounded-full shadow-lg transform transition hover:scale-105 active:scale-95"
|
||||||
>
|
>
|
||||||
<Image
|
Register Now
|
||||||
className="dark:invert"
|
</Link>
|
||||||
src="/vercel.svg"
|
</section>
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-gray-50 py-12 text-center text-gray-500 border-t border-gray-100 relative">
|
||||||
|
<p>© 2026 Vbytes Gaming. All rights reserved.</p>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
322
src/Web/lan-frontend/app/register/page.tsx
Normal file
322
src/Web/lan-frontend/app/register/page.tsx
Normal file
|
|
@ -0,0 +1,322 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
firstName: "",
|
||||||
|
surName: "",
|
||||||
|
grade: "",
|
||||||
|
phoneNumber: "",
|
||||||
|
email: "",
|
||||||
|
guardianName: "",
|
||||||
|
guardianPhoneNumber: "",
|
||||||
|
guardianEmail: "",
|
||||||
|
isVisitor: false,
|
||||||
|
hasApprovedGdpr: false,
|
||||||
|
friends: "",
|
||||||
|
specialDiet: "",
|
||||||
|
ssn: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [message, setMessage] = useState({ type: "", text: "" });
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||||
|
const { name, value, type } = e.target;
|
||||||
|
const val = type === "checkbox" ? (e.target as HTMLInputElement).checked : value;
|
||||||
|
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: val,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formData.ssn) {
|
||||||
|
setMessage({ type: "error", text: "SSN is required for registration." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setMessage({ type: "info", text: "Processing your registration..." });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Check if the SSN has already been registered for the LAN
|
||||||
|
const checkRes = await fetch(`/api/Registration/registered/${formData.ssn}`);
|
||||||
|
const checkData = await checkRes.json();
|
||||||
|
|
||||||
|
if (checkData.isRegistered) {
|
||||||
|
setMessage({ type: "error", text: "This SSN is already registered for the LAN." });
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check membership status via GET Registration register
|
||||||
|
// Returns 200 (Ok) if member, 204 (NoContent) if not
|
||||||
|
const validateRes = await fetch(`/api/Registration/register/${formData.ssn}`);
|
||||||
|
const isMember = validateRes.status === 200;
|
||||||
|
|
||||||
|
// 3. Register the SSN in the membership system via POST
|
||||||
|
await fetch(`/api/Registration/register/${formData.ssn}`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Register the participant for the event
|
||||||
|
const { ssn, ...participantData } = formData;
|
||||||
|
const finalPayload = {
|
||||||
|
...participantData,
|
||||||
|
isMember: isMember
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("/api/Participant/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(finalPayload),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setMessage({ type: "success", text: "Registration complete! You are now registered for the LAN." });
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
setMessage({ type: "error", text: errorData.message || "Event registration failed." });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Registration error:", error);
|
||||||
|
setMessage({ type: "error", text: "A connection error occurred. Please check your internet and try again." });
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-2xl mx-auto bg-white p-8 rounded-xl shadow-md">
|
||||||
|
<div className="mb-6">
|
||||||
|
<Link href="/" className="text-blue-600 hover:text-blue-800 flex items-center gap-1 text-sm font-medium">
|
||||||
|
← Back to Information
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="text-center mb-10">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Lan Registrering</h1>
|
||||||
|
<p className="mt-2 text-gray-600">Vänligen fyll i detta formulär för att anmäla dig till lanet.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-2">
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label htmlFor="ssn" className="block text-sm font-medium text-gray-700">Person Nummer*</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="ssn"
|
||||||
|
id="ssn"
|
||||||
|
placeholder="YYYYMMDD-XXXX"
|
||||||
|
required
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||||
|
value={formData.ssn}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">Required for membership verification. Not stored in event records.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="firstName" className="block text-sm font-medium text-gray-700">First Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="firstName"
|
||||||
|
id="firstName"
|
||||||
|
required
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="surName" className="block text-sm font-medium text-gray-700">Surname *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="surName"
|
||||||
|
id="surName"
|
||||||
|
required
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||||
|
value={formData.surName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="grade" className="block text-sm font-medium text-gray-700">Grade *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="grade"
|
||||||
|
id="grade"
|
||||||
|
required
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||||
|
value={formData.grade}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="phoneNumber" className="block text-sm font-medium text-gray-700">Phone Number</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="phoneNumber"
|
||||||
|
id="phoneNumber"
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||||
|
value={formData.phoneNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2 pt-4 border-t border-gray-100 mt-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Guardian Information</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="guardianName" className="block text-sm font-medium text-gray-700">Guardian Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="guardianName"
|
||||||
|
id="guardianName"
|
||||||
|
required
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||||
|
value={formData.guardianName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="guardianPhoneNumber" className="block text-sm font-medium text-gray-700">Guardian Phone *</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="guardianPhoneNumber"
|
||||||
|
id="guardianPhoneNumber"
|
||||||
|
required
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||||
|
value={formData.guardianPhoneNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label htmlFor="guardianEmail" className="block text-sm font-medium text-gray-700">Guardian Email *</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="guardianEmail"
|
||||||
|
id="guardianEmail"
|
||||||
|
required
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||||
|
value={formData.guardianEmail}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2 pt-4 border-t border-gray-100 mt-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Additional Details</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label htmlFor="friends" className="block text-sm font-medium text-gray-700">Friends (to sit with)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="friends"
|
||||||
|
id="friends"
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||||
|
value={formData.friends}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label htmlFor="specialDiet" className="block text-sm font-medium text-gray-700">Special Diet / Allergies</label>
|
||||||
|
<textarea
|
||||||
|
name="specialDiet"
|
||||||
|
id="specialDiet"
|
||||||
|
rows={3}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2 border text-gray-900"
|
||||||
|
value={formData.specialDiet}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2 space-y-4 pt-4 mt-4 border-t border-gray-100">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex items-center h-5">
|
||||||
|
<input
|
||||||
|
id="isVisitor"
|
||||||
|
name="isVisitor"
|
||||||
|
type="checkbox"
|
||||||
|
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||||
|
checked={formData.isVisitor}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm">
|
||||||
|
<label htmlFor="isVisitor" className="font-medium text-gray-700">Register as Visitor</label>
|
||||||
|
<p className="text-gray-500">Check this if you are visiting and not bringing a computer.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex items-center h-5">
|
||||||
|
<input
|
||||||
|
id="hasApprovedGdpr"
|
||||||
|
name="hasApprovedGdpr"
|
||||||
|
type="checkbox"
|
||||||
|
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||||
|
checked={formData.hasApprovedGdpr}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm">
|
||||||
|
<label htmlFor="hasApprovedGdpr" className="font-medium text-gray-700">GDPR Consent *</label>
|
||||||
|
<p className="text-gray-500">I agree to the processing of my personal data for the purpose of this event.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message.text && (
|
||||||
|
<div className={`p-4 rounded-md ${message.type === "success" ? "bg-green-50 text-green-800 border border-green-200" :
|
||||||
|
message.type === "error" ? "bg-red-50 text-red-800 border border-red-200" :
|
||||||
|
"bg-blue-50 text-blue-800 border border-blue-200"
|
||||||
|
}`}>
|
||||||
|
{message.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="pt-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:bg-blue-400"
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Registering..." : "Register for LAN"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,22 @@
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'images.unsplash.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/api/:path*',
|
||||||
|
destination: `${process.env.REGISTRATION_API_URL}/api/:path*`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue