Companys are now listing on the companies page.
This commit is contained in:
Vendored
+12
-7
@@ -1,13 +1,18 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
session?: {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
import { user } from "$lib";
|
||||
import { api, user } from "$lib";
|
||||
import { redirect, type Handle } from "@sveltejs/kit";
|
||||
import { access } from "fs";
|
||||
import { a } from "vitest/dist/chunks/suite.d.FvehnV49.js";
|
||||
@@ -7,6 +7,11 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
const accessToken = event.cookies.get("access_token");
|
||||
const refreshToken = event.cookies.get("refresh_token");
|
||||
|
||||
event.locals.session = {
|
||||
accessToken: accessToken || "",
|
||||
refreshToken: refreshToken || "",
|
||||
};
|
||||
|
||||
if (event.url.pathname === "/logout") {
|
||||
event.cookies.delete("access_token", { path: "/" });
|
||||
event.cookies.delete("refresh_token", { path: "/" });
|
||||
|
||||
+3
-166
@@ -1,172 +1,9 @@
|
||||
import { getRequestEvent } from "$app/server";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from "axios";
|
||||
import { PUBLIC_API_URL } from "$env/static/public";
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
|
||||
type GetAccessToken = () => string | null | Promise<string | null>;
|
||||
type SetAccessToken = (token: string | null) => void;
|
||||
type RefreshHandler = () => Promise<string>;
|
||||
type LogoutHandler = () => void;
|
||||
|
||||
interface Handlers {
|
||||
getAccessToken?: GetAccessToken;
|
||||
setAccessToken?: SetAccessToken;
|
||||
refreshHandler?: RefreshHandler;
|
||||
onRefreshFailed?: LogoutHandler;
|
||||
}
|
||||
|
||||
const event = getRequestEvent();
|
||||
const api: AxiosInstance = axios.create({
|
||||
baseURL: process.env.API_URL || "",
|
||||
withCredentials: true,
|
||||
baseURL: PUBLIC_API_URL || "",
|
||||
});
|
||||
|
||||
let handlers: Handlers = {
|
||||
getAccessToken() {
|
||||
if (!event) return null;
|
||||
const token = event.cookies.get("authToken");
|
||||
return token || null;
|
||||
},
|
||||
setAccessToken(token: string | null) {
|
||||
if (!event) return;
|
||||
if (token) {
|
||||
event.cookies.set("authToken", token, { httpOnly: true, path: "/" });
|
||||
}
|
||||
},
|
||||
refreshHandler: async () => {
|
||||
if (!event) throw new Error("No request event available");
|
||||
const refreshToken = event.cookies.get("refreshToken");
|
||||
if (!refreshToken) throw new Error("No refresh token available");
|
||||
|
||||
const response = await api.post(
|
||||
"/auth/refresh",
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
"x-refresh-token": `${refreshToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
const newAuthToken = response.data.data.authToken;
|
||||
const newRefreshToken = response.data.data.refreshToken;
|
||||
|
||||
// Update cookies
|
||||
event.cookies.set("authToken", newAuthToken, { httpOnly: true, path: "/" });
|
||||
event.cookies.set("refreshToken", newRefreshToken, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return newAuthToken;
|
||||
},
|
||||
onRefreshFailed: () => {
|
||||
if (!event) return;
|
||||
event.cookies.delete("authToken", { path: "/" });
|
||||
event.cookies.delete("refreshToken", { path: "/" });
|
||||
|
||||
return redirect(303, "/");
|
||||
},
|
||||
};
|
||||
let isRefreshing = false;
|
||||
let refreshPromise: Promise<string> | null = null;
|
||||
const queue: {
|
||||
resolve: (value?: unknown) => void;
|
||||
reject: (err: unknown) => void;
|
||||
config: AxiosRequestConfig;
|
||||
}[] = [];
|
||||
|
||||
export function registerAuthHandlers(h: Handlers) {
|
||||
handlers = { ...handlers, ...h };
|
||||
}
|
||||
|
||||
async function getToken(): Promise<string | null> {
|
||||
if (!handlers.getAccessToken) return null;
|
||||
return handlers.getAccessToken();
|
||||
}
|
||||
|
||||
function setToken(token: string | null) {
|
||||
if (!handlers.setAccessToken) return;
|
||||
handlers.setAccessToken(token);
|
||||
}
|
||||
|
||||
function enqueueRequest(p: {
|
||||
resolve: (value?: unknown) => void;
|
||||
reject: (err: unknown) => void;
|
||||
config: AxiosRequestConfig;
|
||||
}) {
|
||||
queue.push(p);
|
||||
}
|
||||
|
||||
function processQueue(error: unknown, token: string | null = null) {
|
||||
while (queue.length) {
|
||||
const p = queue.shift();
|
||||
if (!p) continue;
|
||||
if (error) p.reject(error);
|
||||
else {
|
||||
if (token && p.config.headers) {
|
||||
(p.config.headers as Record<string, string>)["Authorization"] =
|
||||
`Bearer ${token}`;
|
||||
}
|
||||
p.resolve(api.request(p.config));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
api.interceptors.request.use(
|
||||
async (config) => {
|
||||
const token = await getToken();
|
||||
if (token && config.headers) {
|
||||
(config.headers as Record<string, string>)["Authorization"] =
|
||||
`Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(err) => Promise.reject(err),
|
||||
);
|
||||
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
async (
|
||||
error: AxiosError & { config?: AxiosRequestConfig & { _retry?: boolean } },
|
||||
) => {
|
||||
const originalConfig = error.config;
|
||||
if (!originalConfig) return Promise.reject(error);
|
||||
|
||||
const status = error.response?.status;
|
||||
if (status === 401 && !originalConfig._retry) {
|
||||
originalConfig._retry = true;
|
||||
|
||||
if (!handlers.refreshHandler) {
|
||||
handlers.onRefreshFailed?.();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
refreshPromise = handlers.refreshHandler!()
|
||||
.then((newToken) => {
|
||||
setToken(newToken);
|
||||
processQueue(null, newToken);
|
||||
return newToken;
|
||||
})
|
||||
.catch((err) => {
|
||||
processQueue(err);
|
||||
handlers.onRefreshFailed?.();
|
||||
throw err;
|
||||
})
|
||||
.finally(() => {
|
||||
isRefreshing = false;
|
||||
refreshPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
enqueueRequest({ resolve, reject, config: originalConfig });
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export { api };
|
||||
export default api;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import api from "./axios";
|
||||
|
||||
export const company = {
|
||||
async fetch() {},
|
||||
async fetchMany(accessToken: string, page: number = 1) {
|
||||
const companies = await api.get("/v1/company/companies", {
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
return companies.data;
|
||||
},
|
||||
};
|
||||
+2
-4
@@ -1,10 +1,8 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
|
||||
/**
|
||||
* @TODO MAKE THIS WORK
|
||||
*/
|
||||
//export * from "./axios";
|
||||
export * from "./axios";
|
||||
export * from "./user";
|
||||
export * from "./companies";
|
||||
|
||||
/**
|
||||
* @TODO
|
||||
|
||||
@@ -28,6 +28,8 @@ export const user = {
|
||||
return refreshedTokens;
|
||||
},
|
||||
|
||||
fetchInfo() {},
|
||||
|
||||
logout(event: RequestEvent) {
|
||||
if (!event) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { LayoutServerLoad } from "./$types";
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
// WARNING: returning tokens to the client exposes them to JavaScript.
|
||||
// Prefer keeping tokens httpOnly and proxying requests via server endpoints.
|
||||
return {
|
||||
session: {
|
||||
accessToken: locals.session?.accessToken ?? null,
|
||||
refreshToken: locals.session?.refreshToken ?? null,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,32 +1,94 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { company } from "$lib";
|
||||
import LoadingSpinner from "$lib/../components/LoadingSpinner.svelte";
|
||||
|
||||
// Dummy data - 100 companies for pagination demo
|
||||
const dummyCompanies = Array.from({ length: 100 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: `Company ${i + 1}`,
|
||||
cw_CompanyId: `CW-${String(i + 1).padStart(5, "0")}`,
|
||||
cw_Identifier: `ID-${String(i + 1).padStart(5, "0")}`,
|
||||
}));
|
||||
export let data;
|
||||
|
||||
interface Company {
|
||||
id: string;
|
||||
name: string;
|
||||
cw_CompanyId: number;
|
||||
cw_Identifier: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
data: Company[];
|
||||
successful: boolean;
|
||||
meta: {
|
||||
timestamp: number;
|
||||
pagination: {
|
||||
previousPage: number | null;
|
||||
currentPage: number;
|
||||
nextPage: number | null;
|
||||
totalPages: number;
|
||||
totalRecords: number;
|
||||
listedRecords: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
let companies: Company[] = [];
|
||||
let totalPages = 0;
|
||||
let currentPage = 1;
|
||||
let totalRecords = 0;
|
||||
let isLoading = true;
|
||||
let error: string | null = null;
|
||||
let searchQuery = "";
|
||||
const itemsPerPage = 30; // 3 columns x 10 rows
|
||||
|
||||
$: totalPages = Math.ceil(dummyCompanies.length / itemsPerPage);
|
||||
$: startIndex = (currentPage - 1) * itemsPerPage;
|
||||
$: endIndex = startIndex + itemsPerPage;
|
||||
$: displayedCompanies = dummyCompanies.slice(startIndex, endIndex);
|
||||
const itemsPerPage = 30;
|
||||
|
||||
async function loadCompanies(page: number = 1) {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await company.fetchMany(
|
||||
data.session.accessToken || "",
|
||||
page,
|
||||
);
|
||||
|
||||
if (response && response.data && Array.isArray(response.data)) {
|
||||
companies = response.data;
|
||||
totalRecords =
|
||||
response.meta?.pagination?.totalRecords || response.data.length;
|
||||
totalPages =
|
||||
response.meta?.pagination?.totalPages ||
|
||||
Math.ceil(companies.length / itemsPerPage);
|
||||
currentPage = page;
|
||||
} else {
|
||||
error = "Failed to load companies";
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch companies:", err);
|
||||
error = "Error loading companies. Please try again.";
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: displayedCompanies = companies.filter(
|
||||
(c) =>
|
||||
c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
c.cw_Identifier.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
c.cw_CompanyId.toString().includes(searchQuery),
|
||||
);
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages) {
|
||||
currentPage = page;
|
||||
loadCompanies(page);
|
||||
}
|
||||
}
|
||||
|
||||
function signOut() {
|
||||
goto("/logout");
|
||||
}
|
||||
|
||||
// Load companies on component mount
|
||||
loadCompanies();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -46,68 +108,108 @@
|
||||
<main class="container">
|
||||
<section class="hero">
|
||||
<h2>Company Directory</h2>
|
||||
<p>Browse all companies. Total: {dummyCompanies.length} companies</p>
|
||||
{#if isLoading}
|
||||
<LoadingSpinner loading={true} />
|
||||
{:else if error}
|
||||
<p class="error">{error}</p>
|
||||
{:else}
|
||||
<p>Browse all companies. Total: {totalRecords} companies</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="search-section">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search companies by name, ID, or identifier..."
|
||||
bind:value={searchQuery}
|
||||
class="search-bar"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="companies-grid">
|
||||
{#each displayedCompanies as company (company.id)}
|
||||
<article class="company-card">
|
||||
<h3>{company.name}</h3>
|
||||
<dl>
|
||||
<dt>ID</dt>
|
||||
<dd>{company.id}</dd>
|
||||
<dt>CW Company ID</dt>
|
||||
<dd>{company.cw_CompanyId}</dd>
|
||||
<dt>CW Identifier</dt>
|
||||
<dd>{company.cw_Identifier}</dd>
|
||||
</dl>
|
||||
<a href="/company/{company.id}" class="view-link">View Details</a>
|
||||
</article>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
{#if totalPages > 1}
|
||||
<section class="pagination">
|
||||
<button
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
class="pagination-btn"
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
|
||||
<div class="page-numbers">
|
||||
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as page}
|
||||
<button
|
||||
on:click={() => goToPage(page)}
|
||||
class="page-number {page === currentPage ? 'active' : ''}"
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
class="pagination-btn"
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
|
||||
<span class="page-info">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
{#if !isLoading && !error}
|
||||
<section class="search-section">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search companies by name, ID, or identifier..."
|
||||
bind:value={searchQuery}
|
||||
class="search-bar"
|
||||
/>
|
||||
{#if searchQuery}
|
||||
<p class="search-results">
|
||||
Found {displayedCompanies.length} company/companies matching "{searchQuery}"
|
||||
</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if displayedCompanies.length > 0}
|
||||
<section class="companies-grid">
|
||||
{#each displayedCompanies as comp (comp.id)}
|
||||
<article class="company-card">
|
||||
<h3>{comp.name}</h3>
|
||||
<dl>
|
||||
<dt>CW Company ID</dt>
|
||||
<dd>{comp.cw_CompanyId}</dd>
|
||||
<dt>CW Identifier</dt>
|
||||
<dd>{comp.cw_Identifier}</dd>
|
||||
<dt>Created</dt>
|
||||
<dd>{new Date(comp.createdAt).toLocaleDateString()}</dd>
|
||||
</dl>
|
||||
<a href="/company/{comp.id}" class="view-link">View Details</a>
|
||||
</article>
|
||||
{/each}
|
||||
</section>
|
||||
{:else}
|
||||
<section class="companies-grid">
|
||||
<p class="no-results">No companies found</p>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if totalPages > 1 && !searchQuery}
|
||||
<section class="pagination">
|
||||
<button
|
||||
on:click={() => goToPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
class="pagination-btn"
|
||||
>
|
||||
⟨⟨ First
|
||||
</button>
|
||||
|
||||
<button
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
class="pagination-btn"
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
|
||||
<div class="page-numbers">
|
||||
{#each Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
|
||||
const startPage = Math.max(1, currentPage - 2);
|
||||
return startPage + i;
|
||||
}) as page}
|
||||
{#if page <= totalPages}
|
||||
<button
|
||||
on:click={() => goToPage(page)}
|
||||
class="page-number {page === currentPage ? 'active' : ''}"
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
class="pagination-btn"
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
|
||||
<button
|
||||
on:click={() => goToPage(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
class="pagination-btn"
|
||||
>
|
||||
Last ⟩⟩
|
||||
</button>
|
||||
|
||||
<span class="page-info">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
@@ -169,6 +271,27 @@
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc2626;
|
||||
padding: 1rem;
|
||||
background: #fee2e2;
|
||||
border-radius: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
color: #6b7280;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user