chore: remove unused UI components, dummy data, and theme provider
This commit is contained in:
+68
@@ -0,0 +1,68 @@
|
||||
import { fetchWithAuth } from "@/lib/auth"
|
||||
import type { FormSummary, FormDetail, CreateFormPayload, UpdateFormPayload } from "@/lib/types"
|
||||
|
||||
const API_BASE = "http://localhost:8080"
|
||||
|
||||
export async function getForms(): Promise<FormSummary[]> {
|
||||
const res = await fetchWithAuth(`${API_BASE}/api/forms`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch forms")
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function getFormById(id: string): Promise<FormDetail> {
|
||||
const res = await fetchWithAuth(`${API_BASE}/api/form/${id}`)
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) {
|
||||
throw new Response("Form not found", { status: 404 })
|
||||
}
|
||||
throw new Error("Failed to fetch form")
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function createForm(payload: CreateFormPayload): Promise<FormDetail> {
|
||||
const res = await fetchWithAuth(`${API_BASE}/api/form`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null)
|
||||
throw new Error(data?.message ?? "Failed to create form")
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function updateForm(id: string, payload: UpdateFormPayload): Promise<FormDetail> {
|
||||
const res = await fetchWithAuth(`${API_BASE}/api/form/${id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null)
|
||||
throw new Error(data?.message ?? "Failed to update form")
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function deleteForm(id: string): Promise<void> {
|
||||
const res = await fetchWithAuth(`${API_BASE}/api/form/${id}`, {
|
||||
method: "DELETE",
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null)
|
||||
throw new Error(data?.message ?? "Failed to delete form")
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface JWTPayload {
|
||||
sub: string
|
||||
email: string
|
||||
name?: string
|
||||
exp: number
|
||||
iat: number
|
||||
}
|
||||
|
||||
export function decodeJWT(token: string): JWTPayload | null {
|
||||
try {
|
||||
const base64Url = token.split(".")[1]
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/")
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split("")
|
||||
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join("")
|
||||
)
|
||||
return JSON.parse(jsonPayload)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function isTokenExpired(token: string): boolean {
|
||||
const payload = decodeJWT(token)
|
||||
if (!payload) return true
|
||||
return Date.now() >= payload.exp * 1000
|
||||
}
|
||||
|
||||
function getStorage(): Storage {
|
||||
if (typeof window === "undefined") return localStorage
|
||||
return localStorage.getItem("remember_me") === "true"
|
||||
? localStorage
|
||||
: sessionStorage
|
||||
}
|
||||
|
||||
function getToken(key: string): string | null {
|
||||
return localStorage.getItem(key) ?? sessionStorage.getItem(key)
|
||||
}
|
||||
|
||||
export function setTokens(accessToken: string, refreshToken: string, remember?: boolean) {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
if (remember !== undefined) {
|
||||
localStorage.setItem("remember_me", String(remember))
|
||||
}
|
||||
|
||||
const storage = remember !== undefined
|
||||
? (remember ? localStorage : sessionStorage)
|
||||
: getStorage()
|
||||
|
||||
localStorage.removeItem("access_token")
|
||||
localStorage.removeItem("refresh_token")
|
||||
sessionStorage.removeItem("access_token")
|
||||
sessionStorage.removeItem("refresh_token")
|
||||
|
||||
storage.setItem("access_token", accessToken)
|
||||
storage.setItem("refresh_token", refreshToken)
|
||||
}
|
||||
|
||||
function clearTokens() {
|
||||
localStorage.removeItem("access_token")
|
||||
localStorage.removeItem("refresh_token")
|
||||
localStorage.removeItem("remember_me")
|
||||
sessionStorage.removeItem("access_token")
|
||||
sessionStorage.removeItem("refresh_token")
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(): Promise<string | null> {
|
||||
if (typeof window === "undefined") return null
|
||||
|
||||
const refreshToken = getToken("refresh_token")
|
||||
if (!refreshToken) {
|
||||
clearTokens()
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("http://localhost:8080/api/auth/refresh", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
clearTokens()
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
setTokens(data.access_token, data.refresh_token)
|
||||
return data.access_token
|
||||
} catch {
|
||||
clearTokens()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getUser(): User | null {
|
||||
if (typeof window === "undefined") return null
|
||||
|
||||
const token = getToken("access_token")
|
||||
if (!token || isTokenExpired(token)) return null
|
||||
|
||||
const payload = decodeJWT(token)
|
||||
if (!payload) return null
|
||||
|
||||
return {
|
||||
id: payload.sub,
|
||||
email: payload.email,
|
||||
name: payload.name,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserAsync(): Promise<User | null> {
|
||||
if (typeof window === "undefined") return null
|
||||
|
||||
let token = getToken("access_token")
|
||||
|
||||
if (!token || isTokenExpired(token)) {
|
||||
token = await refreshAccessToken()
|
||||
if (!token) return null
|
||||
}
|
||||
|
||||
const payload = decodeJWT(token)
|
||||
if (!payload) return null
|
||||
|
||||
return {
|
||||
id: payload.sub,
|
||||
email: payload.email,
|
||||
name: payload.name,
|
||||
}
|
||||
}
|
||||
|
||||
export function getAccessToken(): string | null {
|
||||
if (typeof window === "undefined") return null
|
||||
|
||||
const token = getToken("access_token")
|
||||
if (!token || isTokenExpired(token)) return null
|
||||
return token
|
||||
}
|
||||
|
||||
export async function getAccessTokenAsync(): Promise<string | null> {
|
||||
if (typeof window === "undefined") return null
|
||||
|
||||
let token = getToken("access_token")
|
||||
if (token && !isTokenExpired(token)) return token
|
||||
|
||||
token = await refreshAccessToken()
|
||||
return token
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
const token = getToken("access_token")
|
||||
const refreshToken = getToken("refresh_token")
|
||||
|
||||
try {
|
||||
await fetch("http://localhost:8080/api/auth/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
})
|
||||
} catch {
|
||||
}
|
||||
|
||||
clearTokens()
|
||||
}
|
||||
|
||||
export async function fetchWithAuth(url: string, options: RequestInit = {}) {
|
||||
let token = await getAccessTokenAsync()
|
||||
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
},
|
||||
})
|
||||
|
||||
if (res.status === 401) {
|
||||
token = await refreshAccessToken()
|
||||
if (token) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
export type QuestionType =
|
||||
| "short_text"
|
||||
| "long_text"
|
||||
| "multiple_choice"
|
||||
| "checkbox"
|
||||
| "dropdown"
|
||||
| "date"
|
||||
| "rating"
|
||||
|
||||
export interface Question {
|
||||
id: string
|
||||
type: QuestionType
|
||||
title: string
|
||||
required: boolean
|
||||
options?: string[]
|
||||
}
|
||||
|
||||
export interface Form {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
responseCount: number
|
||||
questions: Question[]
|
||||
}
|
||||
|
||||
export const dummyForms: Form[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "RISTEK Datathon 2025 Registration",
|
||||
description:
|
||||
"Register for RISTEK's flagship data science competition — the biggest student-led Datathon in Indonesia. Open for undergraduate students from all universities.",
|
||||
createdAt: "2026-01-10",
|
||||
updatedAt: "2026-02-05",
|
||||
responseCount: 420,
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: "short_text",
|
||||
title: "Full Name",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: "short_text",
|
||||
title: "University / Institution",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "q3",
|
||||
type: "dropdown",
|
||||
title: "Which track are you registering for?",
|
||||
required: true,
|
||||
options: [
|
||||
"Data Analytics",
|
||||
"Machine Learning",
|
||||
"Data Engineering",
|
||||
"MLOps",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q4",
|
||||
type: "multiple_choice",
|
||||
title: "How did you hear about RISTEK Datathon 2025?",
|
||||
required: false,
|
||||
options: [
|
||||
"Instagram / Social Media",
|
||||
"Friend / Colleague",
|
||||
"Fasilkom UI Website",
|
||||
"Email Newsletter",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q5",
|
||||
type: "checkbox",
|
||||
title:
|
||||
"Which tools/technologies are you comfortable with? (Select all that apply)",
|
||||
required: false,
|
||||
options: [
|
||||
"Python",
|
||||
"SQL",
|
||||
"TensorFlow / PyTorch",
|
||||
"Tableau / Power BI",
|
||||
"Spark / Hadoop",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q6",
|
||||
type: "long_text",
|
||||
title:
|
||||
"Briefly describe your motivation for joining RISTEK Datathon 2025.",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "RISTEK Sisters in Tech 2025 — Mentee Application",
|
||||
description:
|
||||
"Apply to be a mentee in SISTECH 2025, the first student-powered women-only tech mentorship program in Indonesia. Open for female Indonesian citizens aged 17–25.",
|
||||
createdAt: "2026-01-20",
|
||||
updatedAt: "2026-02-15",
|
||||
responseCount: 67,
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: "short_text",
|
||||
title: "Full Name",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: "short_text",
|
||||
title: "Email Address",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "q3",
|
||||
type: "dropdown",
|
||||
title: "Select your preferred career path",
|
||||
required: true,
|
||||
options: [
|
||||
"Product Management",
|
||||
"UI/UX Design",
|
||||
"Software Engineering",
|
||||
"Data Analytics",
|
||||
"Digital Marketing",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q4",
|
||||
type: "rating",
|
||||
title:
|
||||
"How would you rate your current knowledge in your chosen career path?",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "q5",
|
||||
type: "checkbox",
|
||||
title: "What do you hope to gain from SISTECH? (Select all that apply)",
|
||||
required: false,
|
||||
options: [
|
||||
"Mentorship from industry professionals",
|
||||
"Networking opportunities",
|
||||
"Portfolio & project guidance",
|
||||
"Career direction & clarity",
|
||||
"Community support",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q6",
|
||||
type: "long_text",
|
||||
title:
|
||||
"Tell us about yourself and why you want to join RISTEK Sisters in Tech 2025.",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "q7",
|
||||
type: "multiple_choice",
|
||||
title: "What is your current education level?",
|
||||
required: true,
|
||||
options: [
|
||||
"High School / Vocational",
|
||||
"Undergraduate (Year 1–2)",
|
||||
"Undergraduate (Year 3–4)",
|
||||
"Fresh Graduate",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
export type QuestionType =
|
||||
| "short_text"
|
||||
| "long_text"
|
||||
| "multiple_choice"
|
||||
| "checkbox"
|
||||
| "dropdown"
|
||||
| "date"
|
||||
| "rating"
|
||||
|
||||
export interface QuestionOption {
|
||||
id: number
|
||||
label: string
|
||||
position: number
|
||||
}
|
||||
|
||||
export interface Question {
|
||||
id: string
|
||||
type: QuestionType
|
||||
title: string
|
||||
required: boolean
|
||||
position: number
|
||||
options: QuestionOption[]
|
||||
}
|
||||
|
||||
export interface FormSummary {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
response_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface FormDetail {
|
||||
id: string
|
||||
user_id: string
|
||||
title: string
|
||||
description: string
|
||||
response_count: number
|
||||
questions: Question[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface CreateQuestionOption {
|
||||
label: string
|
||||
position: number
|
||||
}
|
||||
|
||||
export interface CreateQuestion {
|
||||
type: QuestionType
|
||||
title: string
|
||||
required: boolean
|
||||
position: number
|
||||
options: CreateQuestionOption[]
|
||||
}
|
||||
|
||||
export interface CreateFormPayload {
|
||||
title: string
|
||||
description: string
|
||||
questions: CreateQuestion[]
|
||||
}
|
||||
|
||||
export interface UpdateFormPayload {
|
||||
title: string
|
||||
description: string
|
||||
questions: CreateQuestion[]
|
||||
}
|
||||
Reference in New Issue
Block a user