diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 373eaf4c..26b189bf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,10 +29,18 @@ jobs: steps: - uses: actions/checkout@v4 + - name: 'Authenticate to Google Cloud' + id: auth + uses: 'google-github-actions/auth@v2' + with: + workload_identity_provider: 'projects/84699750544/locations/global/workloadIdentityPools/github/providers/github' + service_account: 'github-actions@analysis-tools-dev.iam.gserviceaccount.com' + create_credentials_file: true + - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: 'npm' - name: Install dependencies @@ -42,6 +50,8 @@ jobs: run: npm run lint - name: Build project + env: + GOOGLE_APPLICATION_CREDENTIALS: ${{ steps.auth.outputs.credentials_file_path }} run: npm run build deploy: @@ -54,10 +64,12 @@ jobs: - uses: actions/checkout@v4 - name: 'Authenticate to Google Cloud' + id: auth uses: 'google-github-actions/auth@v2' with: workload_identity_provider: 'projects/84699750544/locations/global/workloadIdentityPools/github/providers/github' service_account: 'github-actions@analysis-tools-dev.iam.gserviceaccount.com' + create_credentials_file: true - name: 'Set up Cloud SDK' uses: 'google-github-actions/setup-gcloud@v2' @@ -65,7 +77,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: 'npm' - name: Install dependencies @@ -75,6 +87,8 @@ jobs: run: npm run lint - name: Build project + env: + GOOGLE_APPLICATION_CREDENTIALS: ${{ steps.auth.outputs.credentials_file_path }} run: npm run build - name: 'Configure Docker' @@ -110,12 +124,10 @@ jobs: env: GH_TOKEN: ${{ steps.generate_token.outputs.token }} run: | - echo ${{ secrets.FIREBASE_TOKEN }} | base64 -d > ./credentials.json echo "Building Docker Image with tag $IMAGE_NAME" - docker build --build-arg GH_TOKEN=${{ env.GH_TOKEN }} \ - --build-arg PROJECT_ID=analysis-tools-dev \ + DOCKER_BUILDKIT=1 docker build --build-arg GH_TOKEN=${{ env.GH_TOKEN }} \ + --secret id=gcp_creds,src=${{ steps.auth.outputs.credentials_file_path }} \ -t ${IMAGE_NAME} . - rm ./credentials.json - name: 'Push Docker Image' run: | diff --git a/Dockerfile b/Dockerfile index 6b10381e..1cbbb023 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,21 @@ -FROM node:20 as build +FROM node:24 AS build WORKDIR /src COPY package.json package-lock.json /src/ RUN npm ci -ENV GOOGLE_APPLICATION_CREDENTIALS=/src/credentials.json -ENV FIREBASE_PROJECT_ID=analysis-tools-dev ARG GH_TOKEN -ARG PROJECT_ID COPY . /src # Build runs npm run build-data (prebuild hook) which fetches tools data # from GitHub repos and generates static JSON files, then runs next build -RUN npm run build -RUN rm /src/credentials.json +# Uses BuildKit secret for Firestore credentials during build +RUN --mount=type=secret,id=gcp_creds \ + export GOOGLE_APPLICATION_CREDENTIALS=/run/secrets/gcp_creds && \ + npm run build -FROM node:20 +FROM node:24 WORKDIR /src COPY --from=build /src /src -ENTRYPOINT ["npm", "run", "start"] +ENTRYPOINT ["npm", "run", "start"] \ No newline at end of file diff --git a/Makefile b/Makefile index b87d6b2e..92e658f9 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,12 @@ dev: ## Run development server build: ## Build for production npm run build +.PHONY: docker-build +docker-build: ## Build docker image with GCP credentials secret + @test -n "$(GOOGLE_APPLICATION_CREDENTIALS)" || (echo "GOOGLE_APPLICATION_CREDENTIALS must be set"; exit 1) + DOCKER_BUILDKIT=1 docker build --secret id=gcp_creds,src=$(GOOGLE_APPLICATION_CREDENTIALS) -t analysis-tools-dev . + + .PHONY: start start: ## Start Node server npm run start diff --git a/algolia-index.ts b/algolia-index.ts index a00143a8..7214cf4a 100644 --- a/algolia-index.ts +++ b/algolia-index.ts @@ -85,6 +85,9 @@ function prepareDataForIndexing(toolsData: Tool[]): ApiTool[] { resources: tool.resources, wrapper: tool.wrapper, votes: tool.votes ?? 0, + upVotes: tool.upVotes, + downVotes: tool.downVotes, + upvotePercentage: tool.upvotePercentage, other: tool.other, })); diff --git a/components/tools/listPage/ToolCard/ToolCard.tsx b/components/tools/listPage/ToolCard/ToolCard.tsx index 908d58f0..e443021e 100644 --- a/components/tools/listPage/ToolCard/ToolCard.tsx +++ b/components/tools/listPage/ToolCard/ToolCard.tsx @@ -140,13 +140,11 @@ const ToolCard: FC = ({ tool }) => { ))} - {tool.upvotePercentage !== undefined && ( -
  • - - {tool.upvotePercentage}% upvoted - -
  • - )} +
  • + + {tool.upvotePercentage}% upvoted + +
  • diff --git a/components/tools/listPage/ToolsSidebar/FilterCard/LanguageFilterCard.tsx b/components/tools/listPage/ToolsSidebar/FilterCard/LanguageFilterCard.tsx index 24cdfa05..1b7610b1 100644 --- a/components/tools/listPage/ToolsSidebar/FilterCard/LanguageFilterCard.tsx +++ b/components/tools/listPage/ToolsSidebar/FilterCard/LanguageFilterCard.tsx @@ -21,6 +21,7 @@ export interface LanguageFilterCardProps { options: LanguageFilterOption[]; limit?: number; className?: string; + selectionMode?: 'checkbox' | 'radio'; } const LanguageFilterCard: FC = ({ @@ -30,6 +31,7 @@ const LanguageFilterCard: FC = ({ options, limit = 10, className, + selectionMode, }) => { const { search, @@ -45,6 +47,8 @@ const LanguageFilterCard: FC = ({ // Fade out background when not showing all options const [faded, setFaded] = useState(styles.faded); + const isRadioMode = + selectionMode === 'radio' || heading === 'Popular Languages'; const toggleAll = () => { if (listLimit === 999) { @@ -61,8 +65,12 @@ const LanguageFilterCard: FC = ({ updateFilter(searchFilter, []); }; - const handleCheckboxChange = (value: string) => { + const handleOptionChange = (value: string) => { const searchFilter = filter as SearchFilter; + if (isRadioMode) { + updateFilter(searchFilter, [value]); + return; + } toggleFilter(searchFilter, value); }; @@ -77,13 +85,14 @@ const LanguageFilterCard: FC = ({ return values !== undefined && values.length > 0; }; - // Sort options: checked items first, then by count + // Sort options by popularity; keep radio selection from reordering items const sortedOptions = [...options].sort((a, b) => { - const aChecked = isChecked(a.value); - const bChecked = isChecked(b.value); - if (aChecked && !bChecked) return -1; - if (!aChecked && bChecked) return 1; - // Then sort by count + if (!isRadioMode) { + const aChecked = isChecked(a.value); + const bChecked = isChecked(b.value); + if (aChecked && !bChecked) return -1; + if (!aChecked && bChecked) return 1; + } const aCount = getLanguageCount(a.value); const bCount = getLanguageCount(b.value); return bCount - aCount; @@ -129,13 +138,18 @@ const LanguageFilterCard: FC = ({ return (
  • - handleCheckboxChange(option.value) + handleOptionChange(option.value) } />