diff --git a/.claude/skills/contribute/SKILL.md b/.claude/skills/contribute/SKILL.md
new file mode 100644
index 0000000..a1e0512
--- /dev/null
+++ b/.claude/skills/contribute/SKILL.md
@@ -0,0 +1,41 @@
+---
+name: contribute
+description: Implement design from a screenshot or a definition. Use model Claude Sonnet 4.5 for this task.
+---
+
+# Contribute
+
+## Overview
+
+Implements a design from a screenshot or a definition in a Extend Apps Directory structure.
+
+## When to Use
+
+Use this skill when:
+
+- You want to contribute to Extend Apps Directory.
+- You are not familiar on how to contribute to Extend Apps Directory.
+
+## Implementation Principles
+
+1. Always follow semantic HTML.
+2. DO NOT SKEW FROM THE DESIGN. Use exactly the same style _per element_. There may be flaws in the design, but it is what it is.
+3. DO NOT CREATE ADDITIONAL FILES that are otherwise not required. Focus on the given task at hand.
+4. If the prompt specifies a file (or files) that already exists, do not recreate them.
+5. When running a code snippet (command line), use the following as terminal preference:
+ 1. Windows: Powershell 7.
+ 2. \*nix: Zsh.
+6. Implement responsive design with mobile-first approach using CSS media queries.
+7. For texts in the design, COPY THEM WORD FOR WORD. Do not deviate the text content from the design. Ensure that this not only covers a "free text", but also texts inside cards and text inside buttons. Ensure that there are no missing elements.
+
+## Instructions
+
+Make sure to keep the implementation principles above in mind when doing these steps.
+
+### Step 0: Verify requirements
+
+Ensure that you can establish connection to Figma MCP Server according [to the guide](https://developers.figma.com/docs/figma-mcp-server/remote-server-installation/#claude-code). If there is no MCP server named "figma", terminate the action.
+
+### Step 1: Implement the features
+
+Use the resources obtained from Figma MCP server to implement each of the sections. Ensure high quality of the implementation and it's pixel-perfect to what's provided in the design.
\ No newline at end of file
diff --git a/.claude/skills/create-plan/SKILL.md b/.claude/skills/create-plan/SKILL.md
new file mode 100644
index 0000000..bc310c3
--- /dev/null
+++ b/.claude/skills/create-plan/SKILL.md
@@ -0,0 +1,26 @@
+---
+name: create-plan
+description: "Create a detailed design plan from a raw plan."
+---
+
+# Plan
+
+## Overview
+
+Reads a file from `.claude/raw-plans` and creates a detailed plan in `.claude/plans` with the same file base name.
+
+## When to Use
+
+Use this skill when:
+
+- You want to contribute to Extend Apps Directory.
+- You are not familiar on how to contribute to Extend Apps Directory.
+- You want to make sure that your AI-assisted development can be as accurate as possible (reducing back and forth).
+
+## Flow
+
+Given a file name in the format of `.claude/raw-plans/{name}.md`, read that high-level plan and convert it into a detailed plan `.claude/plans/{name}.md`. Use all of the information in the raw plans and use it into the final plan. The final plan should contain these sections:
+
+1. Overview: an outline of the file.
+2. Module breakdown (non-technical): visual elements, assets required, questions.
+3. Module breakdown (technical): per-section high-level implementation detail.
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 7518933..5a0f7d3 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -2,8 +2,6 @@ name: Validate Extend Apps Directory
on:
pull_request:
- paths:
- - 'app/data/extend-apps-directory.json'
jobs:
validate:
@@ -25,3 +23,6 @@ jobs:
- name: Run prebuild script
run: yarn prebuild
+
+ - name: Build
+ run: yarn build
diff --git a/.gitignore b/.gitignore
index cc7c3b4..9b44b18 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,8 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-.yarn/install-state.gz
\ No newline at end of file
+.yarn/install-state.gz
+
+.claude/settings.local.json
+.claude/plans
+.claude/raw-plans
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..50c924b
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,41 @@
+# Extend Apps Directory
+
+## Prerequisites
+
+- Node.js LTS version, ideally v24
+- Yarn modern (_with_ Corepack)
+
+Always start with `yarn` in the root project to install the dependencies before you run any scripts in `package.json`, if you haven't already.
+
+## Conventions
+
+- Use React for the UI library.
+- Use Bootstrap utility classes, such as `d-flex` or `m-4` to style components.
+- Use Ant Design library for the UI components.
+- Always generate formatted code with Prettier.
+- When generating JavaScript syntaxes, use the syntaxes available from the `module` field from the currently used `tsconfig.json`.
+- For styling, follow these order:
+ - If possible to be styled built-in with Ant Design, use it.
+ - If the Ant Design style can't be overridden with Bootstrap class, then create a `*.module.css` file CLOSE TO THE component that uses it (in the same folder as the component). Sometimes it needs `!important` to override Ant Design styles, only use it when it can't be overridden in a "normal way" (e.g. using higher CSS selector specificity).
+- When running a code snippet (command line), use the following as terminal preference:
+ - Windows: Powershell 7.
+ - \*nix: Bash/Zsh.
+- When it comes to naming variables:
+ - Make sure booleans always start with `is`, `has`, or `can`. For example: `isModalOpen`, `hasQueryParameter`, `canUpdate`.
+ - Make sure functions always start with a verb or if it returns boolean, use boolean-y name. For example: `showModal()`, `hasQueryParameter()`.
+
+### React-specific conventions
+
+- When hiding modals, prefer using `open` prop as opposed to returning `null`. For example:
+
+```tsx
+// Don't do this:
+if (!isOpen) return null
+
+// Instead:
+
+```
+
+## Testing
+
+Run `yarn prebuild && yarn typecheck` from the root project.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..43c994c
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+@AGENTS.md
diff --git a/README.md b/README.md
index 6876780..0b92fb9 100644
--- a/README.md
+++ b/README.md
@@ -50,16 +50,30 @@ yarn start
## Project Structure
-- `app/`: Contains the main application code.
- - `assets/`: Static assets like images and logos.
- - `components/`: Reusable React components.
- - `data/`: JSON data for the Extend apps.
- - `routes/`: Route components for different pages.
- - `types/`: TypeScript type definitions.
- - `app.css`: Global CSS styles.
- - `root.tsx`: The root component of the application.
- - `routes.ts`: Route configuration.
-- `public/`: Public assets that are not processed by Vite.
-- `vite.config.ts`: Vite configuration file.
-- `package.json`: Project dependencies and scripts.
-- `tsconfig.json`: TypeScript configuration.
+- `app/`: Contains the main application code.
+ - `assets/`: Static assets like images and logos.
+ - `components/`: Reusable React components.
+ - `data/`: JSON data for the Extend apps.
+ - `routes/`: Route components for different pages.
+ - `types/`: TypeScript type definitions.
+ - `app.css`: Global CSS styles.
+ - `root.tsx`: The root component of the application.
+ - `routes.ts`: Route configuration.
+- `public/`: Public assets that are not processed by Vite.
+- `vite.config.ts`: Vite configuration file.
+- `package.json`: Project dependencies and scripts.
+- `tsconfig.json`: TypeScript configuration.
+
+## AI integration
+
+### Requirements
+
+- Download Claude Code: https://code.claude.com/docs/en/quickstart
+- Set up Figma MCP server: https://developers.figma.com/docs/figma-mcp-server/remote-server-installation/#claude-code.
+
+### How-to
+
+- Copy `resources/plan.md` into `.claude/raw-plans/{page_name}.md`. For example: `.claude/raw-plans/{feature_name}.md`.
+- Open Claude Code, make sure the model is Sonnet 4.5 (or higher). After that, run this: `/create-plan from .claude/raw-plans/{feature_name}.md.`.
+- Answer the questions provided by the AI agent in the new plan document `.claude/plans/{feature_name}.md`.
+- Inside Claude Code, run `/contribute with the context of .claude/plans/{feature_name}-sonnet.md.`.
diff --git a/app/components/ContributingGuideModal.module.css b/app/components/ContributingGuideModal.module.css
new file mode 100644
index 0000000..e5ddbea
--- /dev/null
+++ b/app/components/ContributingGuideModal.module.css
@@ -0,0 +1,73 @@
+.modal {
+ max-width: 100%;
+}
+
+@media (min-width: 768px) {
+ .modal {
+ width: 640px !important;
+ }
+}
+
+.modalHeader {
+ gap: 12px;
+ padding: 20px 24px;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.modalTitle {
+ margin: 0px !important;
+}
+
+.modalContent {
+ padding: 24px;
+}
+
+.instructionsList {
+ margin: 0;
+ padding-left: 20px;
+ color: #000000a6;
+}
+
+.instructionsList li {
+ margin-bottom: 0;
+ line-height: 32px;
+}
+
+.instructionsList ol {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.instructionsList ol li {
+ line-height: 32px;
+}
+
+.instructionsList a {
+ text-decoration: underline;
+}
+
+@media (max-width: 480px) {
+ :global(.ant-modal),
+ :global(.ant-modal-content) {
+ height: 100%;
+ width: 100vw;
+ margin: 0px !important;
+ top: 0px;
+ }
+
+ :global(.ant-modal-content) {
+ border-radius: 0px !important;
+ }
+
+ :global(.ant-modal-body) {
+ height: 100%;
+ }
+
+ .modalTitle {
+ font-size: 20px !important;
+ }
+
+ .modalContent {
+ min-height: 100vh;
+ }
+}
diff --git a/app/components/ContributingGuideModal.tsx b/app/components/ContributingGuideModal.tsx
new file mode 100644
index 0000000..7e4f2de
--- /dev/null
+++ b/app/components/ContributingGuideModal.tsx
@@ -0,0 +1,129 @@
+import { CloseOutlined, ExportOutlined, VideoCameraFilled } from '@ant-design/icons'
+import { Alert, Button, Modal, Typography } from 'antd'
+import { useEffect } from 'react'
+import { useSearchParams } from 'react-router-dom'
+import styles from './ContributingGuideModal.module.css'
+import { CONTRIBUTING_GUIDELINE_MODAL_QUERY_PARAMETER } from '~/types/ui'
+
+const { Title, Paragraph, Text } = Typography
+
+// External link constants
+const LINKS = {
+ PUBLIC_FOLDER: 'https://github.com/AccelByte/extend-apps-directory/tree/main/public',
+ JSON_DATA: 'https://github.com/AccelByte/extend-apps-directory/blob/main/app/data/extend-apps-directory.json',
+ REPOSITORY: 'https://github.com/AccelByte/extend-apps-directory',
+ VIDEO: 'https://www.youtube.com/watch?v=Y3YU5g6ri5I'
+} as const
+
+export function ContributingGuideModal() {
+ const [searchParams, setSearchParams] = useSearchParams()
+ const isModalOpen = searchParams.get(CONTRIBUTING_GUIDELINE_MODAL_QUERY_PARAMETER) === 'true'
+
+ useEffect(() => {
+ // Track modal view with Google Analytics
+ if (isModalOpen && window.gtag) {
+ window.gtag('event', 'view_contributing_guide', {
+ event_category: 'engagement',
+ event_label: 'Contributing Guide Modal Opened'
+ })
+ }
+ }, [isModalOpen])
+
+ const handleClose = () => {
+ searchParams.delete(CONTRIBUTING_GUIDELINE_MODAL_QUERY_PARAMETER)
+ setSearchParams(searchParams, { preventScrollReset: true })
+ }
+
+ return (
+
+
+
+ Submit your app
+
+
+
+
+
+
+
+ Follow these steps to submit a Pull Request (PR) to our team for review and approval:
+
+
+
+ Go public: Ensure your repository (e.g., GitHub) is set to public.
+
+
+ Add app icon: Upload your icon{' '}
+
+
+ here
+
+
+ . Recommended: 512x512px.
+
+
+ Register app or suite: Add your app's details in extend-apps-directory.json{' '}
+
+
+ here
+
+
+ .
+
+
+ App: A single app hosted in one repository.
+
+
+ Suite: A collection of multiple related apps (e.g., Gacha Suite).
+
+
+
+
+ Submit PR: Open a Pull Request to this{' '}
+
+
+ repository
+
+
+ , which our team will review.
+
+
+ Go live: Once approved and merged, your app will automatically appear in the directory.
+
+
+
+ }
+ message={
+
+ Build your first Extend app in minutes using AI.
+
+
+ }
+ showIcon
+ />
+
+
+
+ )
+}
diff --git a/app/components/ExtendDirectoryDetailModal.tsx b/app/components/ExtendDirectoryDetailModal.tsx
index f344456..e4b15ea 100644
--- a/app/components/ExtendDirectoryDetailModal.tsx
+++ b/app/components/ExtendDirectoryDetailModal.tsx
@@ -1,11 +1,10 @@
-import { CloseOutlined } from '@ant-design/icons'
+import { CloseOutlined, ExportOutlined } from '@ant-design/icons'
import { Badge, Button, Card, Modal, Tag, Typography } from 'antd'
import { useSearchParams } from 'react-router-dom'
import type { ExtendDirectoryAppInfo, RepositoryInfo } from '~/types/extend'
import { Language, mapLanguageToColor, mapRepositoryTypeToText } from '~/types/extend'
import extendApps from '../data/extend-apps-directory.json'
import styles from './ExtendDirectoryDetailModal.module.css'
-import { ExternalLink } from './ExternalLink'
import type { ReactNode } from 'react'
const { Title, Text, Paragraph } = Typography
@@ -43,7 +42,7 @@ const Repository = ({ index: isIndexRepository, title, description, url, languag
{elements}
)
diff --git a/app/components/ExternalLink.tsx b/app/components/ExternalLink.tsx
deleted file mode 100644
index e18bf1b..0000000
--- a/app/components/ExternalLink.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import Icon from '@ant-design/icons'
-import { type ComponentProps } from 'react'
-
-const ExternalLinkSvg = () => (
-
-)
-
-export const ExternalLink = (props: Partial>) =>
diff --git a/app/global.d.ts b/app/global.d.ts
new file mode 100644
index 0000000..e861973
--- /dev/null
+++ b/app/global.d.ts
@@ -0,0 +1,6 @@
+// Global type declarations
+
+interface Window {
+ gtag?: (command: string, ...args: any[]) => void
+ dataLayer?: any[]
+}
diff --git a/app/routes/home.module.css b/app/routes/home.module.css
index ae1fdec..a6f097a 100644
--- a/app/routes/home.module.css
+++ b/app/routes/home.module.css
@@ -14,3 +14,13 @@
.title {
color: #000000e0;
}
+
+.contributingLink {
+ color: #0b6cff;
+ text-decoration: underline;
+}
+
+.contributingLink:hover {
+ color: #0b6cff;
+ text-decoration: underline;
+}
diff --git a/app/routes/home.tsx b/app/routes/home.tsx
index 08064b6..c34d2a0 100644
--- a/app/routes/home.tsx
+++ b/app/routes/home.tsx
@@ -1,11 +1,13 @@
import { Select, Typography } from 'antd'
-import { useSearchParams } from 'react-router-dom'
+import { Link, useSearchParams } from 'react-router-dom'
+import { ContributingGuideModal } from '~/components/ContributingGuideModal'
import { ExtendDirectoryDetailModal } from '~/components/ExtendDirectoryDetailModal'
import accelByteLogo from '../assets/accelbyte.svg'
import { ExtendDirectoryItems } from '../components/ExtendDirectoryItems'
import extendAppsJson from '../data/extend-apps-directory.json'
import { type ExtendDirectoryAppInfo, ExternalInfo, FilterDevelopedBy } from '../types/extend'
import styles from './home.module.css'
+import { CONTRIBUTING_GUIDELINE_MODAL_QUERY_PARAMETER } from '~/types/ui'
const { Title, Text } = Typography
@@ -34,7 +36,7 @@ export default function Home() {
const extendApps =
developedBy && developedBy !== FilterDevelopedBy.All
- ? extendAppsJson.filter((e) => {
+ ? extendAppsJson.filter(e => {
if (developedBy === FilterDevelopedBy.External) {
return e.creator !== FilterDevelopedBy.AccelByte
}
@@ -62,7 +64,13 @@ export default function Home() {
Extend Apps Directory
- Enhance your games with powerful Extend apps crafted by our community.
+
+ Power up your game with community-built Extend apps. Have an app to share?{' '}
+
+ Learn how to build and submit an app
+
+ .
+