diff --git a/.github/workflows/create-discussions.yml b/.github/workflows/create-discussions.yml new file mode 100644 index 0000000..f05463f --- /dev/null +++ b/.github/workflows/create-discussions.yml @@ -0,0 +1,218 @@ +name: Create GitHub Discussions + +on: + pull_request: + types: [opened] + paths: + - 'src/content/apps/**' + workflow_dispatch: + inputs: + createForExisting: + description: 'Create discussions for existing apps' + required: true + default: 'true' + type: boolean + +jobs: + create-discussion: + runs-on: ubuntu-latest + permissions: + discussions: write + pull-requests: read + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Create Discussion for PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + async function ensureDiscussionCategory() { + // Get all discussion categories + const { data: categories } = await github.rest.discussions.listRepoCategories({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + // Look for "App Voting" category + let category = categories.find(c => c.name === "App Voting"); + + // Create category if it doesn't exist + if (!category) { + const { data: newCategory } = await github.rest.discussions.createRepoCategory({ + owner: context.repo.owner, + repo: context.repo.repo, + name: "App Voting", + description: "Vote for community apps", + format: "discussion" + }); + category = newCategory; + } + + return category.id; + } + + async function createDiscussion(content) { + // Parse the frontmatter manually + const frontmatterMatch = content.match(/---\n([\s\S]*?)\n---/); + if (!frontmatterMatch) return; + + const frontmatter = frontmatterMatch[1]; + // Parse the simple YAML format manually + const appData = {}; + frontmatter.split('\n').forEach(line => { + const match = line.match(/^(\w+):\s*"?([^"]*)"?$/); + if (match) { + appData[match[1]] = match[2]; + } + }); + + if (!appData.name || !appData.author) { + console.log('Missing required fields in frontmatter'); + return; + } + + // Check if discussion already exists - FIXED API CALL + const { data: existingDiscussions } = await github.rest.search.discussionsQuery({ + owner: context.repo.owner, + repo: context.repo.repo, + query: `repo:${context.repo.owner}/${context.repo.repo} "${appData.name} by ${appData.author}" in:title` + }); + + const existingDiscussion = existingDiscussions.items?.find(d => + d.title === `Vote: ${appData.name} by ${appData.author}` + ); + + const categoryId = await ensureDiscussionCategory(); + + if (!existingDiscussion) { + // Create a discussion - FIXED API CALL + await github.rest.discussions.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `Vote: ${appData.name} by ${appData.author}`, + body: `🗳️ **Vote for this app by giving it a 👍 reaction!**\n\n${appData.description}`, + category_id: categoryId + }); + console.log(`Created discussion for ${appData.name}`); + } else { + console.log(`Discussion already exists for ${appData.name}`); + } + } + + // Get the changed files from the PR + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + + // Find new app submissions + const appFiles = files.filter(file => + file.status === 'added' && + file.filename.startsWith('src/content/apps/') && + !file.filename.endsWith('_template.md') + ); + + for (const file of appFiles) { + const content = Buffer.from((await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: file.filename, + ref: context.payload.pull_request.head.sha + })).data.content, 'base64').toString(); + + await createDiscussion(content); + } + + - name: Create Discussions for Existing Apps + if: github.event_name == 'workflow_dispatch' && inputs.createForExisting + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + async function ensureDiscussionCategory() { + const { data: categories } = await github.rest.discussions.listRepoCategories({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + let category = categories.find(c => c.name === "App Voting"); + + if (!category) { + const { data: newCategory } = await github.rest.discussions.createRepoCategory({ + owner: context.repo.owner, + repo: context.repo.repo, + name: "App Voting", + description: "Vote for community apps", + format: "discussion" + }); + category = newCategory; + } + + return category.id; + } + + async function createDiscussion(content) { + // Parse the frontmatter manually + const frontmatterMatch = content.match(/---\n([\s\S]*?)\n---/); + if (!frontmatterMatch) return; + + const frontmatter = frontmatterMatch[1]; + // Parse the simple YAML format manually + const appData = {}; + frontmatter.split('\n').forEach(line => { + const match = line.match(/^(\w+):\s*"?([^"]*)"?$/); + if (match) { + appData[match[1]] = match[2]; + } + }); + + if (!appData.name || !appData.author) { + console.log('Missing required fields in frontmatter'); + return; + } + + // Check if discussion already exists - FIXED API CALL + const { data: existingDiscussions } = await github.rest.search.discussionsQuery({ + owner: context.repo.owner, + repo: context.repo.repo, + query: `repo:${context.repo.owner}/${context.repo.repo} "${appData.name} by ${appData.author}" in:title` + }); + + const existingDiscussion = existingDiscussions.items?.find(d => + d.title === `Vote: ${appData.name} by ${appData.author}` + ); + + const categoryId = await ensureDiscussionCategory(); + + if (!existingDiscussion) { + // Create a discussion - FIXED API CALL + await github.rest.discussions.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `Vote: ${appData.name} by ${appData.author}`, + body: `🗳️ **Vote for this app by giving it a 👍 reaction!**\n\n${appData.description}`, + category_id: categoryId + }); + console.log(`Created discussion for ${appData.name}`); + } else { + console.log(`Discussion already exists for ${appData.name}`); + } + } + + // Get all existing app files + const appsDir = 'src/content/apps'; + const files = fs.readdirSync(appsDir) + .filter(file => file.endsWith('.md') && file !== '_template.md'); + + for (const file of files) { + const content = fs.readFileSync(path.join(appsDir, file), 'utf8'); + await createDiscussion(content); + } diff --git a/.github/workflows/update-votes.yml b/.github/workflows/update-votes.yml new file mode 100644 index 0000000..f78539c --- /dev/null +++ b/.github/workflows/update-votes.yml @@ -0,0 +1,59 @@ +name: Update Vote Counts + +on: + schedule: + - cron: '0 */6 * * *' # Run every 6 hours + workflow_dispatch: # Allow manual triggers + +jobs: + update-votes: + runs-on: ubuntu-latest + permissions: + contents: write + discussions: read + steps: + - uses: actions/checkout@v4 + + - name: Update Vote Counts + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs').promises; + const path = require('path'); + const yaml = require('js-yaml'); + + // Get all discussions in the Flutter of the Year category + const discussions = await github.paginate(github.rest.discussions.listForRepo, { + owner: context.repo.owner, + repo: context.repo.repo, + category_id: process.env.DISCUSSION_CATEGORY_ID + }); + + // Create votes.json with the current vote counts + const votes = {}; + for (const discussion of discussions) { + if (discussion.title.startsWith('Vote: ')) { + const appName = discussion.title.replace('Vote: ', '').split(' by ')[0]; + // Count thumbs up reactions + const reactions = await github.rest.reactions.listForDiscussion({ + owner: context.repo.owner, + repo: context.repo.repo, + discussion_number: discussion.number + }); + votes[appName] = reactions.data.filter(r => r.content === '+1').length; + } + } + + // Write the votes to a JSON file + await fs.writeFile( + 'src/data/votes.json', + JSON.stringify(votes, null, 2) + ); + + // Commit and push the changes + const date = new Date().toISOString(); + await exec.exec('git', ['config', 'user.name', 'github-actions[bot]']); + await exec.exec('git', ['config', 'user.email', 'github-actions[bot]@users.noreply.github.com']); + await exec.exec('git', ['add', 'src/data/votes.json']); + await exec.exec('git', ['commit', '-m', `Update vote counts - ${date}`]); + await exec.exec('git', ['push']); diff --git a/src/data/votes.json b/src/data/votes.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/src/data/votes.json @@ -0,0 +1 @@ +{} diff --git a/src/pages/index.astro b/src/pages/index.astro index dd9ee21..852718b 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -2,9 +2,18 @@ import { getCollection } from 'astro:content'; import { Image } from 'astro:assets'; import Layout from '../layouts/Layout.astro'; +import { getVotes, type VoteData } from '../utils/votes'; const currentYear = new Date().getFullYear(); const allApps = await getCollection('apps'); +const votes: VoteData = getVotes(); + +// Sort apps by vote count (descending) +const sortedApps = allApps.sort((a, b) => { + const votesA = votes[a.data.name] || 0; + const votesB = votes[b.data.name] || 0; + return votesB - votesA; +}); --- @@ -51,7 +60,7 @@ const allApps = await getCollection('apps');
- {allApps.map((app) => ( + {sortedApps.map((app) => (
@@ -89,6 +98,13 @@ const allApps = await getCollection('apps'); ))}
+
+

{app.data.name}

+
+ 👍 + {votes[app.data.name] || 0} +
+
))} diff --git a/src/utils/votes.ts b/src/utils/votes.ts new file mode 100644 index 0000000..9205ed5 --- /dev/null +++ b/src/utils/votes.ts @@ -0,0 +1,15 @@ +export interface VoteData { + [appName: string]: number; +} + +export function getVotes(): VoteData { + try { + // During build time, this file will be created by GitHub Actions + // We import it as a module to get the data + const votes = import.meta.glob('/src/data/votes.json', { eager: true }); + return Object.values(votes)[0] as VoteData || {}; + } catch (error) { + console.error('Error reading votes:', error); + return {}; + } +}