Skip to content

Commit a04f040

Browse files
feat: Implement initial monorepo structure with mobile, web, and backend applications, featuring DevCard viewing and interactive platform links.
1 parent e9a462f commit a04f040

21 files changed

Lines changed: 1109 additions & 166 deletions

File tree

apps/backend/src/env.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import process from 'node:process';
2+
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import dotenv from 'dotenv';
5+
6+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
7+
const envPath = path.resolve(__dirname, '../../../.env');
8+
const result = dotenv.config({ path: envPath });
9+
10+
if (result.error) {
11+
console.error('❌ Failed to load .env from:', envPath);
12+
console.error(result.error);
13+
} else {
14+
console.log('✅ Loaded .env from:', envPath);
15+
}

apps/backend/src/routes/auth.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@ export async function authRoutes(app: FastifyInstance) {
1717

1818
app.get('/github', async (request: FastifyRequest, reply: FastifyReply) => {
1919
const redirectUri = `${process.env.BACKEND_URL}/auth/github/callback`;
20+
const clientState = (request.query as any).state || '';
21+
const state = clientState ? `${clientState}_${generateState()}` : generateState();
22+
2023
const params = new URLSearchParams({
21-
client_id: process.env.GITHUB_CLIENT_ID || '',
24+
client_id: (process.env.GITHUB_CLIENT_ID || '').trim(),
2225
redirect_uri: redirectUri,
2326
scope: 'read:user user:email',
24-
state: generateState(),
27+
state,
2528
});
26-
return reply.redirect(`${GITHUB_AUTH_URL}?${params}`);
29+
const authUrl = `${GITHUB_AUTH_URL}?${params}`;
30+
console.log('--- GITHUB OAUTH REDIRECT ---');
31+
console.log('URL:', authUrl);
32+
return reply.redirect(authUrl);
2733
});
2834

2935
app.get('/github/callback', async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => {
@@ -41,8 +47,8 @@ export async function authRoutes(app: FastifyInstance) {
4147
Accept: 'application/json',
4248
},
4349
body: JSON.stringify({
44-
client_id: process.env.GITHUB_CLIENT_ID,
45-
client_secret: process.env.GITHUB_CLIENT_SECRET,
50+
client_id: (process.env.GITHUB_CLIENT_ID || '').trim(),
51+
client_secret: (process.env.GITHUB_CLIENT_SECRET || '').trim(),
4652
code,
4753
redirect_uri: `${process.env.BACKEND_URL}/auth/github/callback`,
4854
}),
@@ -128,15 +134,21 @@ export async function authRoutes(app: FastifyInstance) {
128134

129135
app.get('/google', async (request: FastifyRequest, reply: FastifyReply) => {
130136
const redirectUri = `${process.env.BACKEND_URL}/auth/google/callback`;
137+
const clientState = (request.query as any).state || '';
138+
const state = clientState ? `${clientState}_${generateState()}` : generateState();
139+
131140
const params = new URLSearchParams({
132-
client_id: process.env.GOOGLE_CLIENT_ID || '',
141+
client_id: (process.env.GOOGLE_CLIENT_ID || '').trim(),
133142
redirect_uri: redirectUri,
134143
response_type: 'code',
135144
scope: 'openid email profile',
136-
state: generateState(),
145+
state,
137146
access_type: 'offline',
138147
});
139-
return reply.redirect(`${GOOGLE_AUTH_URL}?${params}`);
148+
const authUrl = `${GOOGLE_AUTH_URL}?${params}`;
149+
console.log('--- GOOGLE OAUTH REDIRECT ---');
150+
console.log('URL:', authUrl);
151+
return reply.redirect(authUrl);
140152
});
141153

142154
app.get('/google/callback', async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => {

apps/backend/src/routes/profiles.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,23 @@ export async function profileRoutes(app: FastifyInstance) {
2121
platformLinks: {
2222
orderBy: { displayOrder: 'asc' },
2323
},
24+
cards: {
25+
where: { isDefault: true },
26+
select: { id: true },
27+
take: 1,
28+
},
2429
},
2530
});
2631

2732
if (!user) {
2833
return reply.status(404).send({ error: 'User not found' });
2934
}
3035

31-
const { provider, providerId, ...profile } = user;
32-
return profile;
36+
const { provider, providerId, ...profileData } = user;
37+
return {
38+
...profileData,
39+
defaultCardId: user.cards[0]?.id || null,
40+
};
3341
});
3442

3543
// ─── Update Profile ───

apps/backend/src/routes/public.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,45 @@ export async function publicRoutes(app: FastifyInstance) {
3939
};
4040
});
4141

42+
// ─── Shared Card View (Direct) ───
43+
44+
app.get('/card/:cardId', async (request: FastifyRequest<{ Params: { cardId: string } }>, reply: FastifyReply) => {
45+
const { cardId } = request.params;
46+
47+
const card = await app.prisma.card.findUnique({
48+
where: { id: cardId },
49+
include: {
50+
user: true,
51+
cardLinks: {
52+
include: { platformLink: true },
53+
orderBy: { displayOrder: 'asc' },
54+
},
55+
},
56+
});
57+
58+
if (!card) {
59+
return reply.status(404).send({ error: 'Card not found' });
60+
}
61+
62+
return {
63+
id: card.id,
64+
title: card.title,
65+
owner: {
66+
username: card.user.username,
67+
displayName: card.user.displayName,
68+
bio: card.user.bio,
69+
avatarUrl: card.user.avatarUrl,
70+
accentColor: card.user.accentColor,
71+
},
72+
links: card.cardLinks.map((cl) => ({
73+
id: cl.platformLink.id,
74+
platform: cl.platformLink.platform,
75+
username: cl.platformLink.username,
76+
url: cl.platformLink.url,
77+
})),
78+
};
79+
});
80+
4281
// ─── Public Card View ───
4382

4483
app.get('/:username/card/:cardId', async (request: FastifyRequest<{ Params: { username: string; cardId: string } }>, reply: FastifyReply) => {

apps/backend/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'dotenv/config';
1+
import './env.js';
22
import { buildApp } from './app.js';
33

44
const PORT = parseInt(process.env.PORT || '3000', 10);

apps/mobile/App.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,33 @@ import { ThemeProvider } from './src/context/ThemeContext';
66
import AuthStack from './src/navigation/AuthStack';
77
import MainTabs from './src/navigation/MainTabs';
88

9+
import { Linking } from 'react-native';
10+
911
function AppContent() {
10-
const { isAuthenticated, isLoading } = useAuth();
12+
const { isAuthenticated, isLoading, login } = useAuth();
13+
14+
React.useEffect(() => {
15+
const handleDeepLink = (event: { url: string }) => {
16+
console.log('--- DEEP LINK RECEIVED ---');
17+
console.log('URL:', event.url);
18+
const url = new URL(event.url);
19+
const token = url.searchParams.get('token');
20+
if (token) {
21+
console.log('Token found, logging in...');
22+
login(token);
23+
}
24+
};
25+
26+
const subscription = Linking.addEventListener('url', handleDeepLink);
27+
28+
Linking.getInitialURL().then((url) => {
29+
if (url) handleDeepLink({ url });
30+
});
31+
32+
return () => {
33+
subscription.remove();
34+
};
35+
}, [login]);
1136

1237
if (isLoading) {
1338
return null; // Splash screen could go here
Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +0,0 @@
1-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2-
3-
<uses-permission android:name="android.permission.INTERNET" />
4-
5-
<application
6-
android:name=".MainApplication"
7-
android:label="@string/app_name"
8-
android:icon="@mipmap/ic_launcher"
9-
android:roundIcon="@mipmap/ic_launcher_round"
10-
android:allowBackup="false"
11-
android:theme="@style/AppTheme"
12-
android:usesCleartextTraffic="${usesCleartextTraffic}"
13-
android:supportsRtl="true">
14-
<activity
15-
android:name=".MainActivity"
16-
android:label="@string/app_name"
17-
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
18-
android:launchMode="singleTask"
19-
android:windowSoftInputMode="adjustResize"
20-
android:exported="true">
21-
<intent-filter>
22-
<action android:name="android.intent.action.MAIN" />
23-
<category android:name="android.intent.category.LAUNCHER" />
24-
</intent-filter>
25-
</activity>
26-
</application>
27-
</manifest>

apps/mobile/android/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
3232
# your application. You should enable this flag either if you want
3333
# to write custom TurboModules/Fabric components OR use libraries that
3434
# are providing them.
35-
newArchEnabled=true
35+
newArchEnabled=false
3636

3737
# Use this property to enable or disable the Hermes JS engine.
3838
# If set to false, you will be using JSC instead.

apps/mobile/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,20 @@
1111
},
1212
"dependencies": {
1313
"@devcard/shared": "workspace:*",
14+
"@react-native/new-app-screen": "0.84.1",
1415
"@react-navigation/bottom-tabs": "^7.0.0",
1516
"@react-navigation/native": "^7.0.0",
1617
"@react-navigation/native-stack": "^7.0.0",
1718
"react": "19.2.3",
19+
"react-dom": "^19.2.4",
1820
"react-native": "0.84.1",
1921
"react-native-qrcode-svg": "^6.3.0",
2022
"react-native-safe-area-context": "^5.5.2",
2123
"react-native-screens": "^4.0.0",
2224
"react-native-svg": "^15.0.0",
2325
"react-native-vector-icons": "^10.0.0",
24-
"react-native-webview": "^13.0.0",
25-
"react-native-camera-kit": "^14.0.0",
26-
"@react-native/new-app-screen": "0.84.1"
26+
"react-native-web": "^0.21.2",
27+
"react-native-webview": "^13.0.0"
2728
},
2829
"devDependencies": {
2930
"@babel/core": "^7.25.2",
@@ -33,7 +34,9 @@
3334
"@react-native-community/cli-platform-android": "20.1.0",
3435
"@react-native-community/cli-platform-ios": "20.1.0",
3536
"@react-native/babel-preset": "0.84.1",
37+
"@react-native/codegen": "0.84.1",
3638
"@react-native/eslint-config": "0.84.1",
39+
"@react-native/gradle-plugin": "0.84.1",
3740
"@react-native/metro-config": "0.84.1",
3841
"@react-native/typescript-config": "0.84.1",
3942
"@types/jest": "^29.5.13",

apps/mobile/src/config.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
// DevCard API Configuration
22

3-
// For Android emulator, use 10.0.2.2 instead of localhost
4-
// For physical device, use machine's IP address
3+
// For Android emulator, use localhost with 'adb reverse tcp:3000 tcp:3000'
54
export const API_BASE_URL = __DEV__
6-
? 'http://10.0.2.2:3000'
5+
? 'http://localhost:3000'
76
: 'https://api.devcard.dev';
87

98
export const APP_URL = __DEV__
10-
? 'http://10.0.2.2:5173'
9+
? 'http://localhost:5173'
1110
: 'https://devcard.dev';
1211

1312
export const OAUTH_REDIRECT_URI = 'devcard://oauth/callback';

0 commit comments

Comments
 (0)