Companys are now listing on the companies page.
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
# Copilot Instructions — electron-svelte (SveltronKit)
|
||||||
|
|
||||||
|
This repo is an Electron + SvelteKit app (SveltronKit). The goal of this document is to give an AI coding agent immediate, actionable knowledge about architecture, conventions, and common tasks so suggestions and changes are correct and context-aware.
|
||||||
|
|
||||||
|
Big picture
|
||||||
|
|
||||||
|
- App = Electron main + SvelteKit renderer. See [electron/main.ts](electron/main.ts) and renderer sources under `src/`.
|
||||||
|
- Renderer is a normal SvelteKit app but configured to use the hash router — all routed links expected in the app should begin with `#/` when constructing absolute links for the packaged Electron app (see README).
|
||||||
|
- Electron main process loads the Vite dev server when `MAIN_WINDOW_VITE_DEV_SERVER_URL` is set; otherwise loads the built renderer. See [electron/main.ts](electron/main.ts).
|
||||||
|
|
||||||
|
How to run & build (developer workflows)
|
||||||
|
|
||||||
|
- Uses pnpm. Always use `pnpm install` first.
|
||||||
|
- Dev (runs Electron + Vite via Electron Forge): `pnpm run start` (invokes `electron-forge start`).
|
||||||
|
- Build & package: `pnpm run package` (builds renderer with `vite build` then runs `electron-forge package`).
|
||||||
|
- Create distributable: `pnpm run make`.
|
||||||
|
- Tests: unit tests via `vitest` and e2e via Playwright. `pnpm run test` runs both (`test:unit` and `test:e2e`). See `package.json` scripts.
|
||||||
|
|
||||||
|
Key files & patterns to reference
|
||||||
|
|
||||||
|
- Electron bootstrap: [electron/main.ts](electron/main.ts) — env vars used: `MAIN_WINDOW_VITE_DEV_SERVER_URL`, `MAIN_WINDOW_VITE_NAME`.
|
||||||
|
- Preload bridge: [electron/preload.ts](electron/preload.ts) (currently empty) — add safe, whitelisted APIs here when exposing functionality to the renderer.
|
||||||
|
- SvelteKit entry routes: `src/routes/` (examples: [src/routes/+page.svelte](src/routes/+page.svelte), [src/routes/companies/+page.svelte](src/routes/companies/+page.svelte)).
|
||||||
|
- API client: [src/lib/axios.ts](src/lib/axios.ts) — `api` is created from `PUBLIC_API_URL` ($env/static/public).
|
||||||
|
- Auth helper: [src/lib/authUri.ts](src/lib/authUri.ts) — example of calling backend `/v1/auth/uri`.
|
||||||
|
- Data access: [src/lib/companies.ts](src/lib/companies.ts) — `fetchMany(accessToken)` demonstrates header usage `Authorization: Bearer <token>`.
|
||||||
|
- Global styles: `src/app.css` (Tailwind is present in the project).
|
||||||
|
- Patches: `patches/` contains `@sveltejs__kit.patch` and is referenced in `package.json` via `patchedDependencies` — don't remove or ignore without verifying its purpose.
|
||||||
|
|
||||||
|
Project-specific conventions and examples
|
||||||
|
|
||||||
|
- Router: Because of the hash-router configuration, when constructing absolute links in a packaged app prefer `#/path` (README highlights this). For client navigation within components prefer `goto()` from `$app/navigation` (see [src/routes/+page.svelte](src/routes/+page.svelte)).
|
||||||
|
- Env & API: Use `PUBLIC_API_URL` (imported from `$env/static/public`) as the renderer-side base URL. For backend calls that require auth, pass `Authorization: Bearer <token>` headers (see `src/lib/companies.ts`).
|
||||||
|
- IPC surface: The preload file is where to expose limited, safe APIs to the renderer. The main process defines windows and startup behavior — avoid adding unsafe globals to the renderer.
|
||||||
|
- TypeScript & Svelte: The project uses TypeScript + Svelte 5 — keep code consistent with existing component patterns (script blocks, `$:` reactivity, `svelte:head`, named +page routes).
|
||||||
|
|
||||||
|
Tests & tooling
|
||||||
|
|
||||||
|
- Unit tests: `vitest` (run `pnpm run test:unit`). Look at `demo.spec.ts` and `src/routes/page.svelte.test.ts` for existing examples.
|
||||||
|
- E2E: Playwright tests live under `e2e/` and are run with `pnpm run test:e2e`.
|
||||||
|
- Sync: `prepare` script runs `svelte-kit sync`. Running `pnpm run check` runs `svelte-check` too; use it when editing routes/type-heavy code.
|
||||||
|
|
||||||
|
When making changes, be conservative
|
||||||
|
|
||||||
|
- Changing build or packaging behavior affects developers' ability to run the app locally. Prefer edits to renderer code (under `src/`) unless the user asked to adjust packaging.
|
||||||
|
- If adding new native electron APIs, update `electron/preload.ts` to expose a minimal API surface and document it.
|
||||||
|
|
||||||
|
Useful snippets / concrete examples
|
||||||
|
|
||||||
|
- Navigate programmatically: `import { goto } from "$app/navigation"; goto('/logout');` (see [src/routes/+page.svelte](src/routes/+page.svelte)).
|
||||||
|
- API client usage: `import { api } from 'src/lib/axios'; const res = await api.get('/v1/...');`
|
||||||
|
- Auth redirect fetch: `const { uri, callbackKey } = await fetchAuthRedirectUri(apiUrl);` (see [src/lib/authUri.ts](src/lib/authUri.ts)).
|
||||||
|
|
||||||
|
If anything is ambiguous or you need additional examples (tests, CI, or a missing preload implementation), ask the maintainer which behavior they want (safe IPC surface, packaging targets, or CI test matrix) before making large changes.
|
||||||
|
|
||||||
|
-- End
|
||||||
Vendored
+6
-1
@@ -3,7 +3,12 @@
|
|||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
interface Locals {
|
||||||
|
session?: {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
|
|||||||
+6
-1
@@ -1,4 +1,4 @@
|
|||||||
import { user } from "$lib";
|
import { api, user } from "$lib";
|
||||||
import { redirect, type Handle } from "@sveltejs/kit";
|
import { redirect, type Handle } from "@sveltejs/kit";
|
||||||
import { access } from "fs";
|
import { access } from "fs";
|
||||||
import { a } from "vitest/dist/chunks/suite.d.FvehnV49.js";
|
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 accessToken = event.cookies.get("access_token");
|
||||||
const refreshToken = event.cookies.get("refresh_token");
|
const refreshToken = event.cookies.get("refresh_token");
|
||||||
|
|
||||||
|
event.locals.session = {
|
||||||
|
accessToken: accessToken || "",
|
||||||
|
refreshToken: refreshToken || "",
|
||||||
|
};
|
||||||
|
|
||||||
if (event.url.pathname === "/logout") {
|
if (event.url.pathname === "/logout") {
|
||||||
event.cookies.delete("access_token", { path: "/" });
|
event.cookies.delete("access_token", { path: "/" });
|
||||||
event.cookies.delete("refresh_token", { path: "/" });
|
event.cookies.delete("refresh_token", { path: "/" });
|
||||||
|
|||||||
+3
-166
@@ -1,172 +1,9 @@
|
|||||||
import { getRequestEvent } from "$app/server";
|
import { PUBLIC_API_URL } from "$env/static/public";
|
||||||
import { redirect } from "@sveltejs/kit";
|
import axios, { AxiosInstance } from "axios";
|
||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } 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({
|
const api: AxiosInstance = axios.create({
|
||||||
baseURL: process.env.API_URL || "",
|
baseURL: PUBLIC_API_URL || "",
|
||||||
withCredentials: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 { api };
|
||||||
export default 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.
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
|
|
||||||
/**
|
export * from "./axios";
|
||||||
* @TODO MAKE THIS WORK
|
|
||||||
*/
|
|
||||||
//export * from "./axios";
|
|
||||||
export * from "./user";
|
export * from "./user";
|
||||||
|
export * from "./companies";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @TODO
|
* @TODO
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export const user = {
|
|||||||
return refreshedTokens;
|
return refreshedTokens;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fetchInfo() {},
|
||||||
|
|
||||||
logout(event: RequestEvent) {
|
logout(event: RequestEvent) {
|
||||||
if (!event) return;
|
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">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
import { company } from "$lib";
|
||||||
|
import LoadingSpinner from "$lib/../components/LoadingSpinner.svelte";
|
||||||
|
|
||||||
// Dummy data - 100 companies for pagination demo
|
export let data;
|
||||||
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")}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
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 currentPage = 1;
|
||||||
|
let totalRecords = 0;
|
||||||
|
let isLoading = true;
|
||||||
|
let error: string | null = null;
|
||||||
let searchQuery = "";
|
let searchQuery = "";
|
||||||
const itemsPerPage = 30; // 3 columns x 10 rows
|
|
||||||
|
|
||||||
$: totalPages = Math.ceil(dummyCompanies.length / itemsPerPage);
|
const itemsPerPage = 30;
|
||||||
$: startIndex = (currentPage - 1) * itemsPerPage;
|
|
||||||
$: endIndex = startIndex + itemsPerPage;
|
async function loadCompanies(page: number = 1) {
|
||||||
$: displayedCompanies = dummyCompanies.slice(startIndex, endIndex);
|
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) {
|
function goToPage(page: number) {
|
||||||
if (page >= 1 && page <= totalPages) {
|
if (page >= 1 && page <= totalPages) {
|
||||||
currentPage = page;
|
loadCompanies(page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function signOut() {
|
function signOut() {
|
||||||
goto("/logout");
|
goto("/logout");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load companies on component mount
|
||||||
|
loadCompanies();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -46,9 +108,16 @@
|
|||||||
<main class="container">
|
<main class="container">
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<h2>Company Directory</h2>
|
<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>
|
||||||
|
|
||||||
|
{#if !isLoading && !error}
|
||||||
<section class="search-section">
|
<section class="search-section">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -56,27 +125,46 @@
|
|||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
class="search-bar"
|
class="search-bar"
|
||||||
/>
|
/>
|
||||||
|
{#if searchQuery}
|
||||||
|
<p class="search-results">
|
||||||
|
Found {displayedCompanies.length} company/companies matching "{searchQuery}"
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{#if displayedCompanies.length > 0}
|
||||||
<section class="companies-grid">
|
<section class="companies-grid">
|
||||||
{#each displayedCompanies as company (company.id)}
|
{#each displayedCompanies as comp (comp.id)}
|
||||||
<article class="company-card">
|
<article class="company-card">
|
||||||
<h3>{company.name}</h3>
|
<h3>{comp.name}</h3>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>ID</dt>
|
|
||||||
<dd>{company.id}</dd>
|
|
||||||
<dt>CW Company ID</dt>
|
<dt>CW Company ID</dt>
|
||||||
<dd>{company.cw_CompanyId}</dd>
|
<dd>{comp.cw_CompanyId}</dd>
|
||||||
<dt>CW Identifier</dt>
|
<dt>CW Identifier</dt>
|
||||||
<dd>{company.cw_Identifier}</dd>
|
<dd>{comp.cw_Identifier}</dd>
|
||||||
|
<dt>Created</dt>
|
||||||
|
<dd>{new Date(comp.createdAt).toLocaleDateString()}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<a href="/company/{company.id}" class="view-link">View Details</a>
|
<a href="/company/{comp.id}" class="view-link">View Details</a>
|
||||||
</article>
|
</article>
|
||||||
{/each}
|
{/each}
|
||||||
</section>
|
</section>
|
||||||
|
{:else}
|
||||||
|
<section class="companies-grid">
|
||||||
|
<p class="no-results">No companies found</p>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if totalPages > 1}
|
{#if totalPages > 1 && !searchQuery}
|
||||||
<section class="pagination">
|
<section class="pagination">
|
||||||
|
<button
|
||||||
|
on:click={() => goToPage(1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
class="pagination-btn"
|
||||||
|
>
|
||||||
|
⟨⟨ First
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click={() => goToPage(currentPage - 1)}
|
on:click={() => goToPage(currentPage - 1)}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
@@ -86,13 +174,18 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="page-numbers">
|
<div class="page-numbers">
|
||||||
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as page}
|
{#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
|
<button
|
||||||
on:click={() => goToPage(page)}
|
on:click={() => goToPage(page)}
|
||||||
class="page-number {page === currentPage ? 'active' : ''}"
|
class="page-number {page === currentPage ? 'active' : ''}"
|
||||||
>
|
>
|
||||||
{page}
|
{page}
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -104,11 +197,20 @@
|
|||||||
Next →
|
Next →
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={() => goToPage(totalPages)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
class="pagination-btn"
|
||||||
|
>
|
||||||
|
Last ⟩⟩
|
||||||
|
</button>
|
||||||
|
|
||||||
<span class="page-info">
|
<span class="page-info">
|
||||||
Page {currentPage} of {totalPages}
|
Page {currentPage} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="container">
|
<footer class="container">
|
||||||
@@ -169,6 +271,27 @@
|
|||||||
color: #6b7280;
|
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 {
|
.search-section {
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user