Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions frontend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
node_modules
.next
out
.git
.gitignore
README.md
.env
.env.local
.env.development
.env.production
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
*.pem
.idea
.vscode
2 changes: 1 addition & 1 deletion frontend/.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
NEXT_PUBLIC_BLOCKFROST_API_KEY=previewzwnjcGmHgYLFmLppEWCrmbhapNtCq4H7
NEXT_PUBLIC_BLOCKFROST_API_KEY=preview6rf9Lym3f9XQrTDnxSBbAGwvz5mNafdz
NETWORK=Preview
4 changes: 4 additions & 0 deletions frontend/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
NEXT_PUBLIC_BLOCKFROST_API_KEY=preview6rf9Lym3f9XQrTDnxSBbAGwvz5mNafdz
NETWORK=Preview
# Leave empty in development to use Next.js rewrites (proxies to backend)
NEXT_PUBLIC_API_URL=
5 changes: 5 additions & 0 deletions frontend/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
NEXT_PUBLIC_BLOCKFROST_API_KEY=preview6rf9Lym3f9XQrTDnxSBbAGwvz5mNafdz
NETWORK=Preview
# Production: Set this to your public API endpoint (e.g., https://api.programmabletokens.xyz)
# This should be the publicly accessible URL for your backend API
NEXT_PUBLIC_API_URL=https://preview-api.programmabletokens.xyz
41 changes: 41 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Build stage
FROM node:20-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci

# Copy source files
COPY . .

# Accept build arguments
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_BLOCKFROST_API_KEY
ARG NETWORK=Preview

# Set environment variables for build
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_BLOCKFROST_API_KEY=${NEXT_PUBLIC_BLOCKFROST_API_KEY}
ENV NETWORK=${NETWORK}

# Build the Next.js app (static export)
RUN npm run build

# Production stage - serve with nginx
FROM nginx:alpine

# Copy custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf

# Copy built static files from builder stage
COPY --from=builder /app/out /usr/share/nginx/html

# Expose port 80
EXPOSE 80

# Start nginx
CMD ["nginx", "-g", "daemon off;"]
19 changes: 19 additions & 0 deletions frontend/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash

set -x

VERSION=$(git describe --tags --always --dirty)

echo "Building version: ${VERSION}"

DOCKER_IMAGE_NAME=easy1staking/programmable-tokens-ui
DOCKER_IMAGE="${DOCKER_IMAGE_NAME}:${VERSION}"
DOCKER_IMAGE_LATEST="${DOCKER_IMAGE_NAME}:latest"

docker build -t "${DOCKER_IMAGE}" \
-t "${DOCKER_IMAGE_LATEST}" \
--build-arg NEXT_PUBLIC_API_URL=https://preview-api.programmabletokens.xyz \
--build-arg NEXT_PUBLIC_BLOCKFROST_API_KEY=preview6rf9Lym3f9XQrTDnxSBbAGwvz5mNafdz \
--build-arg NETWORK=Preview \
--push \
.
15 changes: 15 additions & 0 deletions frontend/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: '3.8'

services:
frontend:
build:
context: .
dockerfile: Dockerfile
args:
- NEXT_PUBLIC_API_URL=http://localhost:8080
- NEXT_PUBLIC_BLOCKFROST_API_KEY=preview63R2hq5OToB1PRYyxDQ1NpmS23rbyS88
- NETWORK=Preview
ports:
- "3000:80"
environment:
- NODE_ENV=production
23 changes: 12 additions & 11 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,23 @@ module.exports = (phase, {defaultConfig}) => {
]
},
async rewrites() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
return [
{
source: '/api/v1/:path*', // Match all routes starting with /api/v1/
destination: 'http://localhost:8080/api/v1/:path*', // Proxy to backend server
destination: `${apiUrl}/api/v1/:path*`, // Proxy to backend server
},
];
},
async redirects() {
return [
{
source: '/',
destination: '/connected-wallet',
permanent: true, // Use true for a 301 redirect, false for 302
},
];
},
async redirects() {
return [
{
source: '/',
destination: '/connected-wallet',
permanent: true, // Use true for a 301 redirect, false for 302
},
];
},
experimental: {
esmExternals: true, // Ensure modern module support
},
Expand All @@ -87,4 +88,4 @@ module.exports = (phase, {defaultConfig}) => {
"@lucid-evolution/lucid"
]
}
}
}
42 changes: 42 additions & 0 deletions frontend/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

sendfile on;
keepalive_timeout 65;
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;

# Redirect root to /mint-authority
location = / {
return 301 /mint-authority.html;
}

# Enable SPA routing - serve HTML files for routes
location / {
try_files $uri $uri.html $uri/ =404;
}

# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
}
15 changes: 8 additions & 7 deletions frontend/src/app/[username]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { useContext, useEffect, useState, useMemo } from 'react';

//Axios imports
import axios from 'axios';
import { getApiUrl } from '../utils/apiConfig';

//Mui imports
import { Box, Checkbox, CircularProgress, FormControl, FormControlLabel, InputLabel, MenuItem, Paper, Select, Typography } from '@mui/material';
Expand Down Expand Up @@ -365,7 +366,7 @@ export default function Profile() {
};
try {
const response = await axios.post(
'/api/v1/tx/programmable-token/transfer',
getApiUrl('/api/v1/tx/programmable-token/transfer'),
requestData,
{
headers: {
Expand Down Expand Up @@ -448,7 +449,7 @@ export default function Profile() {
setIsRegistering(true);
changeAlertInfo({ severity: 'info', message: 'Preparing registration transaction…', open: true, link: '' });
const response = await axios.post(
'/api/v1/tx/programmable-token/register-transfer-scripts',
getApiUrl('/api/v1/tx/programmable-token/register-transfer-scripts'),
{ issuer: issuerAddress },
{ headers: { 'Content-Type': 'application/json;charset=utf-8' } }
);
Expand Down Expand Up @@ -502,7 +503,7 @@ export default function Profile() {
setIsInitializingBlacklist(true);
changeAlertInfo({ severity: 'info', message: 'Preparing blacklist initialisation transaction…', open: true, link: '' });
const response = await axios.post(
'/api/v1/tx/programmable-token/blacklist-init',
getApiUrl('/api/v1/tx/programmable-token/blacklist-init'),
{ issuer: issuerAddress },
{ headers: { 'Content-Type': 'application/json;charset=utf-8' } }
);
Expand Down Expand Up @@ -595,7 +596,7 @@ export default function Profile() {
setIsUserMinting(true);
try {
const response = await axios.post(
'/api/v1/tx/programmable-token/issue',
getApiUrl('/api/v1/tx/programmable-token/issue'),
requestData,
{
headers: {'Content-Type': 'application/json;charset=utf-8'}
Expand Down Expand Up @@ -641,7 +642,7 @@ export default function Profile() {
setIsUserFreezing(true);
try {
const response = await axios.post(
'/api/v1/tx/programmable-token/blacklist',
getApiUrl('/api/v1/tx/programmable-token/blacklist'),
requestData,
{ headers: {'Content-Type': 'application/json;charset=utf-8'} }
);
Expand Down Expand Up @@ -694,7 +695,7 @@ export default function Profile() {
setIsUserUnfreezing(true);
try {
const response = await axios.post(
'/api/v1/tx/programmable-token/unblacklist',
getApiUrl('/api/v1/tx/programmable-token/unblacklist'),
requestData,
{ headers: {'Content-Type': 'application/json;charset=utf-8'} }
);
Expand Down Expand Up @@ -747,7 +748,7 @@ export default function Profile() {
setIsUserSeizing(true);
try {
const response = await axios.post(
'/api/v1/tx/programmable-token/seize',
getApiUrl('/api/v1/tx/programmable-token/seize'),
requestData,
{ headers: {'Content-Type': 'application/json;charset=utf-8'} }
);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/lib/demoEnvironment.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ let cachedDemoEnvironment: DemoEnvironment | null = null;

const resolveBaseUrl = () => {
const raw =
process.env.NEXT_PUBLIC_API_URL ?? // Production API URL
process.env.API_BASE_URL ??
process.env.NEXT_PUBLIC_API_BASE_URL ??
process.env.NEXT_PUBLIC_BACKEND_BASE_URL ??
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/app/utils/apiConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Get the API base URL
* - In development: returns empty string (uses Next.js rewrites)
* - In production: returns NEXT_PUBLIC_API_URL for direct API calls
*/
export const getApiBaseUrl = (): string => {
return process.env.NEXT_PUBLIC_API_URL || '';
};

/**
* Build a full API endpoint URL
* @param path - The API path (e.g., '/api/v1/query/address/...')
* @returns Full URL for the API endpoint
*/
export const getApiUrl = (path: string): string => {
const baseUrl = getApiBaseUrl();
return `${baseUrl}${path}`;
};
23 changes: 12 additions & 11 deletions frontend/src/app/utils/walletUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import axios, { AxiosResponse } from 'axios';
import { Address, Assets, Blockfrost, CML, credentialToAddress, credentialToRewardAddress, Lucid, LucidEvolution, makeTxSignBuilder, Network, paymentCredentialOf, scriptHashToCredential, toText, toUnit, TxSignBuilder, Unit, valueToAssets, walletFromSeed } from "@lucid-evolution/lucid";
import type { Credential as LucidCredential, ScriptHash } from "@lucid-evolution/core-types";
import { WalletBalance, DemoEnvironment } from '../store/types';
import { getApiUrl } from './apiConfig';

export async function makeLucid(demoEnvironment: DemoEnvironment) {
const API_KEY_ENV = process.env.NEXT_PUBLIC_BLOCKFROST_API_KEY;
Expand Down Expand Up @@ -38,10 +39,10 @@ export type UserBalanceResponse =
export async function getWalletBalance(demoEnv: DemoEnvironment, address: string): Promise<WalletBalance> {
try {
const response = await axios.get<UserBalanceResponse>(
`/api/v1/query/user-funds/${address}`,
getApiUrl(`/api/v1/query/user-funds/${address}`),
{
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Content-Type': 'application/json;charset=utf-8',
},
}
);
Expand All @@ -62,10 +63,10 @@ export async function getWalletBalance(demoEnv: DemoEnvironment, address: string
export async function getBlacklist(address: string){
try {
const response = await axios.get(
`/api/v1/query/blacklist/${address}`,
getApiUrl(`/api/v1/query/blacklist/${address}`),
{
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Content-Type': 'application/json;charset=utf-8',
},
}
);
Expand All @@ -78,7 +79,7 @@ export async function getBlacklist(address: string){
}

export async function getProgrammableTokenAddress(address: string): Promise<string> {
const response = await axios.get<string>(`/api/v1/query/address/${address}`,
const response = await axios.get<string>(getApiUrl(`/api/v1/query/address/${address}`),
{
headers: {
'Content-Type': 'application/json;charset=utf-8',
Expand All @@ -88,7 +89,7 @@ export async function getProgrammableTokenAddress(address: string): Promise<stri
}

export async function getFreezePolicyId(address: string): Promise<string> {
const response = await axios.get<string>(`/api/v1/query/freeze-policy-id/${address}`,
const response = await axios.get<string>(getApiUrl(`/api/v1/query/freeze-policy-id/${address}`),
{
headers: {
'Content-Type': 'application/json;charset=utf-8',
Expand All @@ -98,7 +99,7 @@ export async function getFreezePolicyId(address: string): Promise<string> {
}

export async function getPolicyIssuer(policyId: string): Promise<string> {
const response = await axios.get<string>(`/api/v1/query/policy-issuer/${policyId}`, {
const response = await axios.get<string>(getApiUrl(`/api/v1/query/policy-issuer/${policyId}`), {
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
Expand All @@ -113,7 +114,7 @@ const trimTrailingSlash = (url: string) => url.replace(/\/+$/, '');

export async function getStakeScriptHashes(address: string): Promise<ScriptHash[]> {
try {
const response = await axios.get<ScriptHash[]>(`/api/v1/query/stake-scripts/${address}`, {
const response = await axios.get<ScriptHash[]>(getApiUrl(`/api/v1/query/stake-scripts/${address}`), {
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
Expand Down Expand Up @@ -237,7 +238,7 @@ export async function getUserTotalProgrammableValue(address: string): Promise<Po
}
try {
const response = await axios.get<UserProgrammableValueResponse>(
`/api/v1/query/user-total-programmable-value/${address}`,
getApiUrl(`/api/v1/query/user-total-programmable-value/${address}`),
{
headers: {
'Content-Type': 'application/json;charset=utf-8',
Expand Down Expand Up @@ -331,15 +332,15 @@ export async function fetchPolicyHolders(

export async function submitTx(tx: string): Promise<AxiosResponse<any, any>> {
return axios.post(
'/api/v1/tx/submit',
getApiUrl('/api/v1/tx/submit'),
{
description: "",
type: "Tx ConwayEra",
cborHex: tx
},
{
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Content-Type': 'application/json;charset=utf-8',
},
}
);
Expand Down