commit 1528c9fc20c5c00d6efbd38aed1186d6ea5c1a11 Author: DmsAnhr Date: Tue Jan 27 09:31:12 2026 +0700 Initial commit on FE diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb253bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# husky +.husky + +# editor configs (local only) +.vscode/ +.idea/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..394f7d5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{ "plugins": ["prettier-plugin-tailwindcss"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1274df4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# ------------------------------------ +# Base image + pnpm +# ------------------------------------ +FROM node:20-alpine AS base +WORKDIR /app +RUN apk add --no-cache libc6-compat +RUN corepack enable && corepack prepare pnpm@9 --activate + +# ------------------------------------ +# Install dependencies +# ------------------------------------ +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-lock.yaml* ./ +RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ + pnpm install --no-frozen-lockfile + +# ------------------------------------ +# Build the application +# ------------------------------------ +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Jadikan build lebih netral tanpa API URL/SECRET +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN pnpm build + +# ------------------------------------ +# Production image +# ------------------------------------ +FROM node:20-alpine AS runner +WORKDIR /app +RUN apk add --no-cache libc6-compat + +RUN addgroup -g 1001 nodejs && \ + adduser -u 1001 -G nodejs -s /bin/sh -D nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +# => ENV hanya untuk runtime, BUKAN build time! +ENV NODE_ENV=production +ENV HOSTNAME=0.0.0.0 +ENV PORT=4000 + +EXPOSE 4000 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD node -e "require('http').get('http://localhost:4000', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +CMD ["node", "server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/(modules)/(landing)/components/catalog-section/_components/main-mapset-card-skeleton.tsx b/app/(modules)/(landing)/components/catalog-section/_components/main-mapset-card-skeleton.tsx new file mode 100644 index 0000000..3056b99 --- /dev/null +++ b/app/(modules)/(landing)/components/catalog-section/_components/main-mapset-card-skeleton.tsx @@ -0,0 +1,37 @@ +// MainMapsetCardSkeleton.tsx +import { Card, CardContent } from "@/shared/components/ui/card"; + +export function MainMapsetCardSkeleton() { + return ( + + + {/* Map preview placeholder */} +
+ + {/* Category badge */} +
+ + {/* Content */} +
+ {/* Title lines */} +
+
+ + {/* Stats row */} +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + ); +} diff --git a/app/(modules)/(landing)/components/catalog-section/index.tsx b/app/(modules)/(landing)/components/catalog-section/index.tsx new file mode 100644 index 0000000..62ae844 --- /dev/null +++ b/app/(modules)/(landing)/components/catalog-section/index.tsx @@ -0,0 +1,98 @@ +"use client"; + +import React from "react"; +import Link from "next/link"; +import { MainMapsetCard } from "./main-mapset-card"; +import { useQuery } from "@tanstack/react-query"; +import mapsetApi from "@/shared/services/mapset"; +import { Button } from "@/shared/components/button/button"; +import { MainMapsetCardSkeleton } from "./_components/main-mapset-card-skeleton"; +import { ErrorState } from "@/shared/components/error-state"; + +export function CatalogSection() { + const { + data: mapsets, + isLoading, + isError, + error, + isSuccess, + refetch, + isFetching, + } = useQuery({ + queryKey: ["mapsets-catalog"], + queryFn: () => + mapsetApi + .getMapsets( + { + limit: 5, + filter: JSON.stringify([ + "is_active=true", + "status_validation=approved", + ]), + }, + { skipAuth: true }, + ) + .then((res) => { + return res.items; + }), + staleTime: 5000, + retry: 1, + }); + + const showSkeleton = isLoading || (isFetching && !isSuccess); + + return ( + <> +
+
+
+
+

+ Katalog Mapset +

+
+ + + +
+

+ Koleksi lengkap peta dan data geospasial Jawa Timur +

+ + {(() => { + if (showSkeleton) { + return ( +
+ {[...Array(4)].map((_, i) => ( + + ))} +
+ ); + } + + if (isError) { + return ; + } + + if (isSuccess && (!mapsets || mapsets.length === 0)) { + return ( +
+ Belum ada mapset untuk ditampilkan. +
+ ); + } + + if (isSuccess && mapsets && mapsets.length > 0) { + return ( +
+ {mapsets.slice(0, 4).map((mapset) => ( + + ))} +
+ ); + } + })()} +
+ + ); +} diff --git a/app/(modules)/(landing)/components/catalog-section/main-mapset-card.tsx b/app/(modules)/(landing)/components/catalog-section/main-mapset-card.tsx new file mode 100644 index 0000000..a533246 --- /dev/null +++ b/app/(modules)/(landing)/components/catalog-section/main-mapset-card.tsx @@ -0,0 +1,62 @@ +import LoadingSpinner from "@/shared/components/loading-spinner"; +import { CardContent, Card } from "@/shared/components/ui/card"; +import { Mapset } from "@/shared/types/mapset"; +import { Download, Eye } from "lucide-react"; +import dynamic from "next/dynamic"; + +const PreviewMap = dynamic(() => import("@/shared/components/preview-map"), { + ssr: false, + loading: () => , +}); + +export function MainMapsetCard({ mapset }: Readonly<{ mapset: Mapset }>) { + return ( + + +
+ +
+ + {mapset?.category?.name} + + +
+ +

+ {mapset.name} +

+
+ +
+
+
+ + {mapset.view_count} +
+
+ + {mapset.download_count} +
+
+ {/* +
+
+ + 2.450 +
+
+ + 892 +
+
+
*/} +
+
+
+
+ ); +} diff --git a/app/(modules)/(landing)/components/category-section/category-card.tsx b/app/(modules)/(landing)/components/category-section/category-card.tsx new file mode 100644 index 0000000..3d08eff --- /dev/null +++ b/app/(modules)/(landing)/components/category-section/category-card.tsx @@ -0,0 +1,43 @@ +import { getFileThumbnailUrl } from "@/shared/utils/file"; +import Image from "next/image"; +import Link from "next/link"; + +export default function CategoryCard({ + name, + icon, + link, totalDataset, +}: Readonly<{ + id: string; + name: string; + description?: string | null; + icon?: string | null; + link: string; + totalDataset: number; +}>) { + return ( + +
+
+ {name} +
+
+ +
+

{name}

+ +
+ {totalDataset} Dataset +
+
+ + ); +} diff --git a/app/(modules)/(landing)/components/category-section/index.tsx b/app/(modules)/(landing)/components/category-section/index.tsx new file mode 100644 index 0000000..587f953 --- /dev/null +++ b/app/(modules)/(landing)/components/category-section/index.tsx @@ -0,0 +1,88 @@ +"use client"; +import React, { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import categoryApi from "@/shared/services/category"; +import CategoryCard from "./category-card"; +import { ErrorState } from "@/shared/components/error-state"; +import { Button } from "@/shared/components/button/button"; + +export function CategorySection() { + const [showAll, setShowAll] = useState(false); + + const { data: categories, isLoading, isFetching, isSuccess, isError, error, refetch } = useQuery({ + queryKey: ["categories-list", showAll], + queryFn: () => + categoryApi + .getCategories( + { + ...(showAll ? {} : { limit: 6 }), + filter: ["is_active=true"], + }, + { skipAuth: true } + ) + .then((res) => res.items), + staleTime: 5000, + retry: 1, + }); + + const showSkeleton = isLoading || (isFetching && !isSuccess); + + return ( +
+
+
+
+

Topik

+
+ {!showAll ? ( + + ) : ( + + )} +
+

Telusuri ragam topik yang tersedia!

+ + {(() => { + if (showSkeleton) { + return ( +
+ {[...Array(6)].map((_, i) => ( +
+ ))} +
+ ); + } + if (isError) { + return ; + } + if (isSuccess && (!categories || categories.length === 0)) { + return ( +
Belum ada kategori untuk ditampilkan.
+ ); + } + if (isSuccess && categories && categories.length > 0) { + return ( +
+ {categories.map((cat) => ( + + ))} +
+ ); + } + return null; + })()} +
+ ); +} diff --git a/app/(modules)/(landing)/components/feedback/_components/emoji-rating.tsx b/app/(modules)/(landing)/components/feedback/_components/emoji-rating.tsx new file mode 100644 index 0000000..13ce374 --- /dev/null +++ b/app/(modules)/(landing)/components/feedback/_components/emoji-rating.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { Label } from "@/shared/components/ui/label"; // atau pakai