Skip to content

Commit b68b65e

Browse files
authored
docs: update README with frontend setup instructions and features (#12)
* docs: update README with frontend setup instructions and features; refactor API client for improved type safety and consistency * docs: update README with correct repository URL for cloning
1 parent 25f5bd2 commit b68b65e

6 files changed

Lines changed: 217 additions & 121 deletions

File tree

Readme.md

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ The project follows a clean architecture pattern with clear separation of concer
3131

3232
1. Clone the repository:
3333
```bash
34-
git clone <repository-url>
34+
git clone git@github.com:eulixir/lnk.git
3535
cd lnk
3636
```
3737

@@ -49,6 +49,12 @@ This will start:
4949
- Redis on port `6379`
5050
- Cassandra on port `9042`
5151

52+
> **⚠️ Important**: The Cassandra setup can take a significant amount of time (30-60 seconds or more) to fully initialize and be ready to accept connections. Wait for Cassandra to be healthy before starting the backend application. You can check readiness with:
53+
> ```bash
54+
> docker exec lnk-cassandra nodetool status
55+
> ```
56+
> When Cassandra is ready, you should see the node status as `UN` (Up Normal).
57+
5258
4. Create a `.env` file in the project root with the following configuration:
5359
5460
```env
@@ -235,8 +241,121 @@ CREATE TABLE urls (
235241

236242
This design ensures fast lookups when retrieving URLs by their short code.
237243

244+
## Frontend
245+
246+
The frontend is a modern Next.js application that provides a user-friendly interface for the URL shortener service.
247+
248+
### Frontend Features
249+
250+
- 🎨 **Modern UI**: Built with Next.js 16 and React 19
251+
- 🎯 **Type-Safe API Client**: Auto-generated TypeScript client from Swagger/OpenAPI
252+
- 🎨 **Beautiful Components**: Uses shadcn/ui component library
253+
- 📱 **Responsive Design**: Mobile-friendly interface
254+
-**Fast Performance**: Optimized with React Compiler
255+
256+
### Frontend Prerequisites
257+
258+
- Node.js 18+ or Bun
259+
- Backend API running (see Backend section)
260+
261+
### Frontend Installation
262+
263+
1. Navigate to the frontend directory:
264+
```bash
265+
cd frontend
266+
```
267+
268+
2. Install dependencies:
269+
```bash
270+
# Using npm
271+
npm install
272+
273+
# Or using Bun
274+
bun install
275+
```
276+
277+
3. Generate API client from Swagger documentation:
278+
```bash
279+
npm run generate:api
280+
# Or
281+
bun run generate:api
282+
```
283+
284+
**Note**: Make sure the backend is running and Swagger documentation is available at `http://localhost:8080/swagger/doc.json` before generating the API client.
285+
286+
### Running the Frontend
287+
288+
#### Development Mode
289+
290+
```bash
291+
npm run dev
292+
# Or
293+
bun run dev
294+
```
295+
296+
The frontend will start on `http://localhost:3000` (default Next.js port).
297+
298+
#### Production Build
299+
300+
```bash
301+
npm run build
302+
npm run start
303+
# Or
304+
bun run build
305+
bun run start
306+
```
307+
308+
### Frontend Scripts
309+
310+
- `npm run dev` / `bun run dev`: Start development server
311+
- `npm run build` / `bun run build`: Build for production
312+
- `npm run start` / `bun run start`: Start production server
313+
- `npm run lint` / `bun run lint`: Run linter (Biome)
314+
- `npm run lint:fix` / `bun run lint:fix`: Fix linting issues
315+
- `npm run format` / `bun run format`: Format code
316+
- `npm run generate:api` / `bun run generate:api`: Generate API client from Swagger
317+
318+
### Frontend Technologies
319+
320+
- **Next.js 16**: React framework with App Router
321+
- **React 19**: UI library
322+
- **TypeScript**: Type safety
323+
- **Tailwind CSS**: Utility-first CSS framework
324+
- **shadcn/ui**: High-quality component library
325+
- **Orval**: OpenAPI client generator
326+
- **Biome**: Fast linter and formatter
327+
- **React Hook Form**: Form management
328+
- **Sonner**: Toast notifications
329+
- **Lucide React**: Icon library
330+
331+
### Frontend Project Structure
332+
333+
```
334+
frontend/
335+
├── src/
336+
│ ├── app/ # Next.js App Router pages
337+
│ │ ├── [shortUrl]/ # Dynamic route for URL redirection
338+
│ │ └── page.tsx # Home page
339+
│ ├── api/ # API client and configuration
340+
│ │ ├── lnk.ts # Auto-generated API client
341+
│ │ └── undici-instance.ts # Custom fetch instance
342+
│ ├── components/ # React components
343+
│ │ ├── ui/ # shadcn/ui components
344+
│ │ ├── url-dialog.tsx
345+
│ │ ├── url-input.tsx
346+
│ │ └── url-shortener.tsx
347+
│ ├── hooks/ # Custom React hooks
348+
│ ├── lib/ # Utility functions
349+
│ └── types/ # TypeScript type definitions
350+
├── public/ # Static assets
351+
├── orval.config.ts # API client generation config
352+
├── next.config.ts # Next.js configuration
353+
└── package.json # Dependencies and scripts
354+
```
355+
238356
## Technologies Used
239357

358+
### Backend
240359
- **Go 1.24**: Programming language
241360
- **Gin**: HTTP web framework
242361
- **Cassandra (gocql)**: Database for URL storage
@@ -246,6 +365,14 @@ This design ensures fast lookups when retrieving URLs by their short code.
246365
- **Docker Compose**: Local development environment
247366
- **Testify**: Testing framework
248367

368+
### Frontend
369+
- **Next.js 16**: React framework
370+
- **React 19**: UI library
371+
- **TypeScript**: Type safety
372+
- **Tailwind CSS**: Styling
373+
- **shadcn/ui**: Component library
374+
- **Orval**: API client generator
375+
249376
## Configuration
250377

251378
The application uses environment variables for configuration. All configuration options can be set in a `.env` file or as environment variables.

frontend/src/api/lnk.ts

Lines changed: 75 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* A URL shortener service API
66
* OpenAPI spec version: 1.0
77
*/
8-
import { customInstance } from './undici-instance';
8+
import { customInstance } from "./undici-instance";
99
export interface HandlersCreateURLRequest {
1010
url: string;
1111
}
@@ -19,140 +19,127 @@ export interface HandlersErrorResponse {
1919
error?: string;
2020
}
2121

22-
export type GetHealth200 = {[key: string]: string};
22+
export type GetHealth200 = { [key: string]: string };
2323

24-
export type GetShortUrl308 = {[key: string]: string};
24+
export type GetShortUrl308 = { [key: string]: string };
2525

26-
export type GetShortUrl500 = {[key: string]: string};
26+
export type GetShortUrl500 = { [key: string]: string };
2727

2828
/**
2929
* Check if the API is running
3030
* @summary Health check endpoint
3131
*/
3232
export type getHealthResponse200 = {
33-
data: GetHealth200
34-
status: 200
35-
}
36-
37-
export type getHealthResponseSuccess = (getHealthResponse200) & {
33+
data: GetHealth200;
34+
status: 200;
35+
};
36+
37+
export type getHealthResponseSuccess = getHealthResponse200 & {
3838
headers: Headers;
3939
};
40-
;
4140

42-
export type getHealthResponse = (getHealthResponseSuccess)
41+
export type getHealthResponse = getHealthResponseSuccess;
4342

4443
export const getGetHealthUrl = () => {
44+
return `/health`;
45+
};
4546

46-
47-
48-
49-
return `/health`
50-
}
51-
52-
export const getHealth = async ( options?: RequestInit): Promise<getHealthResponse> => {
53-
54-
return customInstance<getHealthResponse>(getGetHealthUrl(),
55-
{
47+
export const getHealth = async (
48+
options?: RequestInit,
49+
): Promise<getHealthResponse> => {
50+
return customInstance<getHealthResponse>(getGetHealthUrl(), {
5651
...options,
57-
method: 'GET'
58-
59-
60-
}
61-
);}
62-
63-
52+
method: "GET",
53+
});
54+
};
6455

6556
/**
6657
* Create a short URL from a long URL
6758
* @summary Create a short URL
6859
*/
6960
export type postShortenResponse200 = {
70-
data: HandlersCreateURLResponse
71-
status: 200
72-
}
61+
data: HandlersCreateURLResponse;
62+
status: 200;
63+
};
7364

7465
export type postShortenResponse400 = {
75-
data: HandlersErrorResponse
76-
status: 400
77-
}
66+
data: HandlersErrorResponse;
67+
status: 400;
68+
};
7869

7970
export type postShortenResponse500 = {
80-
data: HandlersErrorResponse
81-
status: 500
82-
}
83-
84-
export type postShortenResponseSuccess = (postShortenResponse200) & {
71+
data: HandlersErrorResponse;
72+
status: 500;
73+
};
74+
75+
export type postShortenResponseSuccess = postShortenResponse200 & {
8576
headers: Headers;
8677
};
87-
export type postShortenResponseError = (postShortenResponse400 | postShortenResponse500) & {
78+
export type postShortenResponseError = (
79+
| postShortenResponse400
80+
| postShortenResponse500
81+
) & {
8882
headers: Headers;
8983
};
9084

91-
export type postShortenResponse = (postShortenResponseSuccess | postShortenResponseError)
85+
export type postShortenResponse =
86+
| postShortenResponseSuccess
87+
| postShortenResponseError;
9288

9389
export const getPostShortenUrl = () => {
90+
return `/shorten`;
91+
};
9492

95-
96-
97-
98-
return `/shorten`
99-
}
100-
101-
export const postShorten = async (handlersCreateURLRequest: HandlersCreateURLRequest, options?: RequestInit): Promise<postShortenResponse> => {
102-
103-
return customInstance<postShortenResponse>(getPostShortenUrl(),
104-
{
93+
export const postShorten = async (
94+
handlersCreateURLRequest: HandlersCreateURLRequest,
95+
options?: RequestInit,
96+
): Promise<postShortenResponse> => {
97+
return customInstance<postShortenResponse>(getPostShortenUrl(), {
10598
...options,
106-
method: 'POST',
107-
headers: { 'Content-Type': 'application/json', ...options?.headers },
108-
body: JSON.stringify(
109-
handlersCreateURLRequest,)
110-
}
111-
);}
112-
113-
99+
method: "POST",
100+
headers: { "Content-Type": "application/json", ...options?.headers },
101+
body: JSON.stringify(handlersCreateURLRequest),
102+
});
103+
};
114104

115105
/**
116106
* Get the original URL from a short URL
117107
* @summary Get original URL by short URL
118108
*/
119109
export type getShortUrlResponse308 = {
120-
data: GetShortUrl308
121-
status: 308
122-
}
110+
data: GetShortUrl308;
111+
status: 308;
112+
};
123113

124114
export type getShortUrlResponse404 = {
125-
data: HandlersErrorResponse
126-
status: 404
127-
}
115+
data: HandlersErrorResponse;
116+
status: 404;
117+
};
128118

129119
export type getShortUrlResponse500 = {
130-
data: GetShortUrl500
131-
status: 500
132-
}
133-
134-
;
135-
export type getShortUrlResponseError = (getShortUrlResponse308 | getShortUrlResponse404 | getShortUrlResponse500) & {
120+
data: GetShortUrl500;
121+
status: 500;
122+
};
123+
export type getShortUrlResponseError = (
124+
| getShortUrlResponse308
125+
| getShortUrlResponse404
126+
| getShortUrlResponse500
127+
) & {
136128
headers: Headers;
137129
};
138130

139-
export type getShortUrlResponse = (getShortUrlResponseError)
140-
141-
export const getGetShortUrlUrl = (shortUrl: string,) => {
142-
131+
export type getShortUrlResponse = getShortUrlResponseError;
143132

144-
145-
146-
return `/${shortUrl}`
147-
}
133+
export const getGetShortUrlUrl = (shortUrl: string) => {
134+
return `/${shortUrl}`;
135+
};
148136

149-
export const getShortUrl = async (shortUrl: string, options?: RequestInit): Promise<getShortUrlResponse> => {
150-
151-
return customInstance<getShortUrlResponse>(getGetShortUrlUrl(shortUrl),
152-
{
137+
export const getShortUrl = async (
138+
shortUrl: string,
139+
options?: RequestInit,
140+
): Promise<getShortUrlResponse> => {
141+
return customInstance<getShortUrlResponse>(getGetShortUrlUrl(shortUrl), {
153142
...options,
154-
method: 'GET'
155-
156-
157-
}
158-
);}
143+
method: "GET",
144+
});
145+
};

frontend/src/api/undici-instance.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,3 @@ export const customInstance = async <T>(
5050
headers: response.headers,
5151
};
5252
};
53-

0 commit comments

Comments
 (0)