From 61ca5b6380f2830ae9e869ff1075ddfd39fc381b Mon Sep 17 00:00:00 2001
From: Kaustav Halder
Date: Fri, 22 May 2026 12:03:53 +0530
Subject: [PATCH 1/3] Implemented profile integration and QR improvements
---
apps/backend/prisma/schema.prisma | 5 +-
apps/backend/src/app.ts | 12 +-
apps/backend/src/plugins/redis.ts | 4 +
apps/backend/src/routes/auth.ts | 52 +
apps/web/src/app.css | 15 +-
apps/web/src/app.html | 7 +
apps/web/src/lib/api.ts | 30 +
apps/web/src/routes/+layout.svelte | 10 +
apps/web/src/routes/+page.svelte | 68 +-
apps/web/src/routes/api/[...path]/+server.ts | 68 +
apps/web/src/routes/dashboard/+page.server.ts | 38 +
apps/web/src/routes/dashboard/+page.svelte | 1196 +++++++++++++++++
apps/web/src/routes/login/+page.svelte | 335 +++++
apps/web/src/routes/u/[username]/+page.svelte | 33 +-
apps/web/svelte.config.js | 2 +-
package.json | 1 +
packages/shared/src/platforms.ts | 26 +
17 files changed, 1845 insertions(+), 57 deletions(-)
create mode 100644 apps/web/src/lib/api.ts
create mode 100644 apps/web/src/routes/api/[...path]/+server.ts
create mode 100644 apps/web/src/routes/dashboard/+page.server.ts
create mode 100644 apps/web/src/routes/dashboard/+page.svelte
create mode 100644 apps/web/src/routes/login/+page.svelte
diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma
index f2d3afe..ebf6dbf 100644
--- a/apps/backend/prisma/schema.prisma
+++ b/apps/backend/prisma/schema.prisma
@@ -2,9 +2,8 @@ generator client {
provider = "prisma-client-js"
}
datasource db {
- provider = "postgresql"
- url = env("DATABASE_URL")
-
+ provider = "sqlite"
+ url = "file:./dev.db"
}
model User {
diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts
index 5f45001..f472d42 100644
--- a/apps/backend/src/app.ts
+++ b/apps/backend/src/app.ts
@@ -34,8 +34,18 @@ export async function buildApp() {
});
// ─── Core Plugins ───
+ const allowedOrigins = [
+ 'http://localhost:5173',
+ 'http://localhost:5174',
+ 'http://127.0.0.1:5173',
+ 'http://127.0.0.1:5174'
+ ];
+ if (process.env.PUBLIC_APP_URL) {
+ allowedOrigins.push(process.env.PUBLIC_APP_URL);
+ }
+
await app.register(cors, {
- origin: process.env.PUBLIC_APP_URL || 'http://localhost:5173',
+ origin: allowedOrigins,
credentials: true,
});
diff --git a/apps/backend/src/plugins/redis.ts b/apps/backend/src/plugins/redis.ts
index c7b6f94..976fb42 100644
--- a/apps/backend/src/plugins/redis.ts
+++ b/apps/backend/src/plugins/redis.ts
@@ -14,6 +14,10 @@ export const redisPlugin = fp(async (app: FastifyInstance) => {
lazyConnect: true,
});
+ redis.on('error', (err) => {
+ app.log.debug(`Redis connection state: offline (${err.message})`);
+ });
+
try {
await redis.connect();
app.log.info('🔴 Redis connected');
diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts
index febc41d..29fb6e0 100644
--- a/apps/backend/src/routes/auth.ts
+++ b/apps/backend/src/routes/auth.ts
@@ -278,6 +278,58 @@ export async function authRoutes(app: FastifyInstance) {
};
});
+ // ─── Local Developer Bypass Auth ───
+
+ app.post('/bypass', async (request: FastifyRequest, reply: FastifyReply) => {
+ const { username } = request.body as { username: string };
+ if (!username) {
+ return reply.status(400).send({ error: 'Missing username' });
+ }
+
+ const cleanUsername = username.trim().toLowerCase().replace(/[^a-zA-Z0-9_-]/g, '');
+ if (!cleanUsername) {
+ return reply.status(400).send({ error: 'Invalid username format' });
+ }
+
+ try {
+ const user = await app.prisma.user.upsert({
+ where: {
+ username: cleanUsername,
+ },
+ update: {},
+ create: {
+ email: `${cleanUsername}@devcard.local`,
+ username: cleanUsername,
+ displayName: username.trim(),
+ bio: 'Full-stack developer building outstanding interfaces.',
+ role: 'Software Engineer',
+ company: 'DevCard Team',
+ avatarUrl: `https://api.dicebear.com/7.x/bottts/svg?seed=${cleanUsername}`,
+ provider: 'dev_bypass',
+ providerId: `bypass_${cleanUsername}`,
+ },
+ });
+
+ const token = app.jwt.sign(
+ { id: user.id, username: user.username },
+ { expiresIn: '30d' }
+ );
+
+ reply.setCookie('token', token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'lax',
+ path: '/',
+ maxAge: 30 * 24 * 60 * 60,
+ });
+
+ return { token, user };
+ } catch (err) {
+ app.log.error('Bypass login error:', err);
+ return reply.status(500).send({ error: 'Authentication bypass failed' });
+ }
+ });
+
// ─── Logout ───
app.post('/logout', async (request: FastifyRequest, reply: FastifyReply) => {
diff --git a/apps/web/src/app.css b/apps/web/src/app.css
index c775623..120b922 100644
--- a/apps/web/src/app.css
+++ b/apps/web/src/app.css
@@ -19,6 +19,11 @@
--text-secondary: #475569;
--text-muted: #64748b;
+ /* Secondary Button & Inputs */
+ --btn-secondary-bg: rgba(15, 23, 42, 0.05);
+ --btn-secondary-border: rgba(15, 23, 42, 0.08);
+ --btn-secondary-hover-bg: rgba(15, 23, 42, 0.09);
+
/* Effects */
--border: rgba(226, 232, 240, 0.9);
--border-glass: rgba(255, 255, 255, 0.35);
@@ -44,6 +49,10 @@ html.dark {
--text-secondary: #cbd5e1;
--text-muted: #64748b;
+ --btn-secondary-bg: rgba(255, 255, 255, 0.08);
+ --btn-secondary-border: rgba(255, 255, 255, 0.14);
+ --btn-secondary-hover-bg: rgba(255, 255, 255, 0.14);
+
--border: rgba(30, 41, 59, 0.85);
--border-glass: rgba(255, 255, 255, 0.12);
}
@@ -131,14 +140,14 @@ button {
padding: 0.85rem 1.75rem;
border-radius: calc(var(--radius) * 1.2);
font-weight: 700;
- border: 1px solid rgba(255, 255, 255, 0.14);
- background: rgba(255, 255, 255, 0.08);
+ border: 1px solid var(--btn-secondary-border);
+ background: var(--btn-secondary-bg);
color: var(--text-primary);
cursor: pointer;
}
.btn-secondary:hover {
- background: rgba(255, 255, 255, 0.14);
+ background: var(--btn-secondary-hover-bg);
border-color: rgba(99, 102, 241, 0.45);
}
diff --git a/apps/web/src/app.html b/apps/web/src/app.html
index f273cc5..ee910ff 100644
--- a/apps/web/src/app.html
+++ b/apps/web/src/app.html
@@ -3,6 +3,13 @@
+
%sveltekit.head%
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
new file mode 100644
index 0000000..c861162
--- /dev/null
+++ b/apps/web/src/lib/api.ts
@@ -0,0 +1,30 @@
+export async function apiFetch(path: string, options: RequestInit = {}) {
+ // Route all browser fetches through the SvelteKit /api/ proxy gateway
+ const cleanPath = path.startsWith('/') ? path.substring(1) : path;
+ const url = `/api/${cleanPath}`;
+
+ const headers = new Headers(options.headers);
+ if (options.body && !(options.body instanceof FormData) && !headers.has('Content-Type')) {
+ headers.set('Content-Type', 'application/json');
+ }
+
+ const response = await fetch(url, {
+ ...options,
+ headers,
+ });
+
+ if (!response.ok) {
+ let errorMsg = 'An error occurred';
+ try {
+ const data = await response.json();
+ errorMsg = data.error || errorMsg;
+ } catch {}
+ throw new Error(errorMsg);
+ }
+
+ if (response.status === 204) {
+ return null;
+ }
+
+ return response.json();
+}
diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte
index ac6dd12..ea7edec 100644
--- a/apps/web/src/routes/+layout.svelte
+++ b/apps/web/src/routes/+layout.svelte
@@ -1,6 +1,16 @@
diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte
index 11f8085..cab348b 100644
--- a/apps/web/src/routes/+page.svelte
+++ b/apps/web/src/routes/+page.svelte
@@ -34,14 +34,17 @@
@@ -52,15 +55,10 @@
The open-source standard for developer networking. Put all your profiles—GitHub, LinkedIn, LeetCode, and more—into a single, high-impact digital card.
@@ -130,8 +128,8 @@
.theme-toggle {
width: 46px;
height: 46px;
- background: rgba(255, 255, 255, 0.08);
- border: 1px solid rgba(255, 255, 255, 0.12);
+ background: var(--btn-secondary-bg);
+ border: 1px solid var(--btn-secondary-border);
border-radius: 50%;
cursor: pointer;
display: inline-flex;
@@ -143,7 +141,7 @@
.theme-toggle:hover {
transform: scale(1.05);
- background: rgba(255, 255, 255, 0.14);
+ background: var(--btn-secondary-hover-bg);
}
.theme-toggle:focus-visible {
@@ -203,13 +201,13 @@
padding: 0.92rem 1.75rem;
border-radius: calc(var(--radius) * 1.15);
font-weight: 700;
- border: 1px solid rgba(255, 255, 255, 0.18);
- background: rgba(255, 255, 255, 0.08);
+ border: 1px solid var(--btn-secondary-border);
+ background: var(--btn-secondary-bg);
color: var(--text-primary);
}
.btn-secondary:hover {
- background: rgba(255, 255, 255, 0.14);
+ background: var(--btn-secondary-hover-bg);
border-color: rgba(99, 102, 241, 0.45);
}
@@ -230,23 +228,25 @@
}
.feature-card {
- padding: 2.4rem;
+ padding: 2rem;
border-radius: var(--radius-xl);
- box-shadow: var(--shadow-lg);
- background: linear-gradient(180deg, rgba(15, 23, 42, 0.75), rgba(15, 23, 42, 0.5));
- border: 1px solid rgba(255, 255, 255, 0.08);
+ box-shadow: var(--shadow-md);
+ background: var(--bg-glass);
+ backdrop-filter: blur(18px);
+ -webkit-backdrop-filter: blur(18px);
+ border: 1px solid var(--border-glass);
transition: transform 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease;
+ min-height: 180px;
+ display: flex;
+ flex-direction: column;
}
- .feature-card {
- min-height: 140px;
- padding: 16px;
-}
-@media (max-width: 640px) {
- .feature-card {
- margin-bottom: 12px;
+ @media (max-width: 640px) {
+ .feature-card {
+ margin-bottom: 12px;
+ padding: 1.5rem;
+ }
}
-}
.feature-card:hover {
transform: translateY(-8px);
diff --git a/apps/web/src/routes/api/[...path]/+server.ts b/apps/web/src/routes/api/[...path]/+server.ts
new file mode 100644
index 0000000..c926bb7
--- /dev/null
+++ b/apps/web/src/routes/api/[...path]/+server.ts
@@ -0,0 +1,68 @@
+import { error } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+
+const API_BASE = process.env.BACKEND_URL || 'http://localhost:3000';
+
+export const fallback: RequestHandler = async ({ params, request, cookies, url }) => {
+ const path = params.path;
+ const token = cookies.get('token');
+
+ // Build headers
+ const headers = new Headers(request.headers);
+ if (token) {
+ headers.set('Authorization', `Bearer ${token}`);
+ }
+
+ // Suppress headers that could conflict during local proxying
+ headers.delete('host');
+ headers.delete('connection');
+
+ // Fastify route resolution: strip any duplicate leading api/ prefixes
+ let cleanPath = path;
+ if (cleanPath.startsWith('api/')) {
+ cleanPath = cleanPath.substring(4);
+ }
+ const targetPath = cleanPath.startsWith('auth/') ? cleanPath : `api/${cleanPath}`;
+ const targetUrl = new URL(`${API_BASE}/${targetPath}${url.search}`);
+
+ try {
+ let requestBody: any = undefined;
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
+ const blob = await request.blob();
+ if (blob.size > 0) {
+ requestBody = blob;
+ } else {
+ headers.delete('content-type');
+ }
+ }
+
+ const res = await fetch(targetUrl.toString(), {
+ method: request.method,
+ headers,
+ body: requestBody,
+ });
+
+ const responseHeaders = new Headers();
+ const contentType = res.headers.get('Content-Type');
+ if (contentType) {
+ responseHeaders.set('Content-Type', contentType);
+ }
+
+ // Forward cookies set by the backend (like token set on login)
+ const setCookie = res.headers.get('Set-Cookie');
+ if (setCookie) {
+ responseHeaders.set('Set-Cookie', setCookie);
+ }
+
+ return new Response(res.body, {
+ status: res.status,
+ headers: responseHeaders,
+ });
+ } catch (err: any) {
+ console.error('DevCard Proxy Error:', err);
+ return new Response(JSON.stringify({ error: 'Backend API is currently offline.' }), {
+ status: 502,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ }
+};
diff --git a/apps/web/src/routes/dashboard/+page.server.ts b/apps/web/src/routes/dashboard/+page.server.ts
new file mode 100644
index 0000000..40e5d90
--- /dev/null
+++ b/apps/web/src/routes/dashboard/+page.server.ts
@@ -0,0 +1,38 @@
+import { redirect } from '@sveltejs/kit';
+import type { PageServerLoad } from './$types';
+
+const API_BASE = process.env.BACKEND_URL || 'http://localhost:3000';
+
+export const load: PageServerLoad = async ({ cookies, fetch }) => {
+ const token = cookies.get('token');
+ if (!token) {
+ throw redirect(302, '/login');
+ }
+
+ try {
+ const res = await fetch(`${API_BASE}/api/profiles/me`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (!res.ok) {
+ // Token is invalid or expired, clear and redirect
+ cookies.delete('token', { path: '/' });
+ throw redirect(302, '/login');
+ }
+
+ const user = await res.json();
+ return { user };
+ } catch (err: any) {
+ // If the error is a SvelteKit redirect instruction, forward it
+ if (err.status && err.status >= 300 && err.status < 400) {
+ throw err;
+ }
+
+ return {
+ user: null,
+ error: 'Backend API is currently unreachable. Please make sure the backend server is running.',
+ };
+ }
+};
diff --git a/apps/web/src/routes/dashboard/+page.svelte b/apps/web/src/routes/dashboard/+page.svelte
new file mode 100644
index 0000000..87a6db0
--- /dev/null
+++ b/apps/web/src/routes/dashboard/+page.svelte
@@ -0,0 +1,1196 @@
+
+
+
+
+
+ {#each toasts as toast (toast.id)}
+
+ {toast.type === 'success' ? '⚡' : '⚠️'}
+ {toast.message}
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+
+ Fine-tune your brand colors and professional details.
+
+
+
+
+
+
+
+ Register or reorder profiles linked directly to your smart card.
+
+
+
+
+
+
+ {#if links.length === 0}
+
+
📭
+
No profiles linked yet
+
Add your LeetCode, GitHub, or LinkedIn pages to get started.
+
+ {:else}
+
+ {#each links as link, index (link.id)}
+ {@const platform = PLATFORMS[link.platform]}
+
+
+ {platform?.name.charAt(0) || link.platform.charAt(0)}
+
+
+
+ @{link.username}
+
+
+
+
+
+
+
+
+
+ {/each}
+
+ {/if}
+
+
+
+
+
+
+ Real-time preview of your public identity page.
+
+
+
+
+
+
+
+ {#if user.avatarUrl}
+

+ {:else}
+
+ {user.displayName.charAt(0).toUpperCase()}
+
+ {/if}
+
{user.displayName}
+ {#if user.role}
+
{user.role}{user.company ? ` @ ${user.company}` : ''}
+ {/if}
+ {#if user.bio}
+
{user.bio}
+ {/if}
+
+
+
+
+
+
+
+
+
Share & Download
+
Copy your direct profile link or export high-resolution QR graphics for printing.
+
+
+
+
+
+
Download QR Code:
+
+
+
+
+
+
+
+
+
+
+
+
+
+{#if isModalOpen}
+ isModalOpen = false}>
+
e.stopPropagation()}>
+
+
+
+
+
+{/if}
+
+
diff --git a/apps/web/src/routes/login/+page.svelte b/apps/web/src/routes/login/+page.svelte
new file mode 100644
index 0000000..6b63775
--- /dev/null
+++ b/apps/web/src/routes/login/+page.svelte
@@ -0,0 +1,335 @@
+
+
+
+ Login | DevCard Studio ⚡
+
+
+
+
+
+
+
+
+
+
+
+
+ Local Development Mode
+
+
External Google & GitHub OAuth secrets are bypassable in localhost environments for seamless setup.
+
+
+
+
+
diff --git a/apps/web/src/routes/u/[username]/+page.svelte b/apps/web/src/routes/u/[username]/+page.svelte
index bb23cca..000c4c8 100644
--- a/apps/web/src/routes/u/[username]/+page.svelte
+++ b/apps/web/src/routes/u/[username]/+page.svelte
@@ -154,7 +154,7 @@
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 0%, var(--accent), transparent 50%),
- #020617;
+ var(--bg-page);
opacity: 0.18;
z-index: -1;
}
@@ -180,11 +180,13 @@
max-width: 540px;
border-radius: var(--radius-xl);
padding: 2.5rem 2rem;
- box-shadow: 0 26px 60px -20px rgba(0, 0, 0, 0.55);
+ box-shadow: var(--shadow-lg);
position: relative;
overflow: hidden;
- border: 1px solid rgba(255, 255, 255, 0.08);
- background: rgba(15, 23, 42, 0.96);
+ border: 1px solid var(--border-glass);
+ background: var(--bg-glass);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
}
.profile-header {
@@ -233,8 +235,8 @@
align-items: center;
justify-content: center;
padding: 0.45rem 1rem;
- background: rgba(255, 255, 255, 0.08);
- border: 1px solid rgba(255, 255, 255, 0.12);
+ background: var(--btn-secondary-bg);
+ border: 1px solid var(--btn-secondary-border);
border-radius: 999px;
font-size: 0.9rem;
font-weight: 700;
@@ -261,9 +263,9 @@
align-items: center;
padding: 1rem;
border-radius: calc(var(--radius) * 1.1);
- border: 1px solid rgba(255, 255, 255, 0.1);
- background: rgba(255, 255, 255, 0.06);
- box-shadow: 0 12px 30px -18px rgba(0, 0, 0, 0.35);
+ border: 1px solid var(--border);
+ background: var(--bg-secondary);
+ box-shadow: var(--shadow-sm);
transition: transform 0.25s ease, background 0.25s ease, border-color 0.25s ease;
animation: slideIn 0.5s ease-out forwards;
animation-delay: var(--delay);
@@ -272,9 +274,10 @@
.link-tile:hover,
.link-tile:focus-visible {
- background: rgba(255, 255, 255, 0.13);
+ background: var(--bg-primary);
transform: translateY(-2px);
- border-color: rgba(99, 102, 241, 0.35);
+ border-color: var(--primary);
+ box-shadow: var(--shadow-md);
}
.link-tile:focus-visible {
@@ -332,7 +335,7 @@
.card-footer {
margin-top: 2.5rem;
padding-top: 1.75rem;
- border-top: 1px solid rgba(255,255,255,0.08);
+ border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
@@ -373,9 +376,9 @@
}
.copy-link-button {
- border: 1px solid var(--border-glass);
+ border: 1px solid var(--btn-secondary-border);
border-radius: var(--radius);
- background: rgba(255, 255, 255, 0.08);
+ background: var(--btn-secondary-bg);
color: var(--text-primary);
cursor: pointer;
font: inherit;
@@ -385,7 +388,7 @@
}
.copy-link-button:hover {
- background: rgba(255, 255, 255, 0.15);
+ background: var(--btn-secondary-hover-bg);
transform: translateY(-1px);
}
diff --git a/apps/web/svelte.config.js b/apps/web/svelte.config.js
index 55c3bd2..38b2fca 100644
--- a/apps/web/svelte.config.js
+++ b/apps/web/svelte.config.js
@@ -14,7 +14,7 @@ const config = {
'script-src': ['self', 'unsafe-inline'],
'style-src': ['self', 'unsafe-inline', 'https://fonts.googleapis.com'],
'img-src': ['self', 'data:', 'https:'],
- 'connect-src': ['self'],
+ 'connect-src': ['self', 'http://localhost:3000', 'ws://localhost:5173'],
'font-src': ['self', 'data:', 'https:', 'https://fonts.gstatic.com'],
'object-src': ['none'],
'base-uri': ['self'],
diff --git a/package.json b/package.json
index bbe44f7..77d8e64 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"description": "One Tap. Every Profile. Every Platform. Open Source Developer Profile Exchange Platform.",
"license": "Apache-2.0",
"scripts": {
+ "dev": "concurrently \"pnpm dev:backend\" \"pnpm dev:web\"",
"dev:backend": "pnpm --filter @devcard/backend dev",
"dev:mobile": "pnpm --filter @devcard/mobile start",
"dev:web": "pnpm --filter @devcard/web dev",
diff --git a/packages/shared/src/platforms.ts b/packages/shared/src/platforms.ts
index a218957..c0de90f 100644
--- a/packages/shared/src/platforms.ts
+++ b/packages/shared/src/platforms.ts
@@ -162,6 +162,32 @@ export const PLATFORMS: Record = {
usernamePlaceholder: 'e.g. coder',
usesFullUrl: false,
},
+ geeksforgeeks: {
+ id: 'geeksforgeeks',
+ name: 'GeeksforGeeks',
+ icon: 'code',
+ color: '#2F8D46',
+ urlPattern: 'https://www.geeksforgeeks.org/user/{username}',
+ deepLinkPattern: null,
+ webViewUrlPattern: null,
+ followStrategy: 'link',
+ oauthScopes: [],
+ usernamePlaceholder: 'e.g. gfg_user',
+ usesFullUrl: false,
+ },
+ codeforces: {
+ id: 'codeforces',
+ name: 'Codeforces',
+ icon: 'award',
+ color: '#1A8FFF',
+ urlPattern: 'https://codeforces.com/profile/{username}',
+ deepLinkPattern: null,
+ webViewUrlPattern: null,
+ followStrategy: 'link',
+ oauthScopes: [],
+ usernamePlaceholder: 'e.g. tourist',
+ usesFullUrl: false,
+ },
hackerrank: {
id: 'hackerrank',
name: 'HackerRank',
From 5e2ad94e458396a3ece676de65f5ee09f7d56b8a Mon Sep 17 00:00:00 2001
From: Kaustav Halder
Date: Sat, 23 May 2026 22:33:07 +0530
Subject: [PATCH 2/3] Resolved merge conflicts
---
apps/web/src/app.html | 5 ++++
apps/web/src/routes/+page.svelte | 45 ++++++++++++++++++--------------
2 files changed, 30 insertions(+), 20 deletions(-)
diff --git a/apps/web/src/app.html b/apps/web/src/app.html
index ee910ff..2165b44 100644
--- a/apps/web/src/app.html
+++ b/apps/web/src/app.html
@@ -3,6 +3,11 @@
+
+
+
+
+