+
+
Recent Activity
+
+
+
+
+
+
-
Dark mode support
-
Requested by 45 users
+
New feature request submitted
+
2 hours ago
-
-
- In Progress
-
- 45 votes
-
-
-
-
-
-
-
+
+
+
-
Mobile app
-
Requested by 32 users
+
Feature marked as completed
+
4 hours ago
-
-
- Planned
-
- 32 votes
-
-
-
-
-
-
-
+
+
+
-
Email notifications
-
Requested by 28 users
+
New user registered
+
1 day ago
-
-
- Under Review
-
-
28 votes
+
+
+
+
+
+
Feature received 10+ upvotes
+
2 days ago
+
-
+
+
+
diff --git a/pages/get-started/index.vue b/pages/get-started/index.vue
new file mode 100644
index 0000000..560c89b
--- /dev/null
+++ b/pages/get-started/index.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Start collecting feedback
+
+ Create a workspace to set up feedback boards, manage projects, and collaborate with your team.
+
+
+
+
+
+
+
Public feedback boards for your users
+
+
+
+
Upvoting and prioritization
+
+
+
+
Team collaboration and project management
+
+
+
+
+ Create your workspace
+
+
+
+
+ Already have an account?
+
+ Sign in
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/login/index.vue b/pages/login/index.vue
index 7a21f47..48cc680 100644
--- a/pages/login/index.vue
+++ b/pages/login/index.vue
@@ -70,7 +70,7 @@
Don't have an account?
- Sign up
+ Sign up
@@ -110,6 +110,12 @@ export default {
error: '',
}
},
+ computed: {
+ signupLink() {
+ const redirect = this.$route.query.redirect
+ return redirect ? `/signup?redirect=${encodeURIComponent(redirect)}` : '/signup'
+ },
+ },
methods: {
async handleSubmit() {
if (!this.email || !this.password) {
@@ -129,8 +135,8 @@ export default {
if (result.error) {
this.error = result.error.message || 'Sign in failed'
} else {
- // Success - redirect to dashboard
- await navigateTo('/dashboard')
+ const redirect = this.$route.query.redirect
+ await navigateTo(redirect && typeof redirect === 'string' && redirect.startsWith('/') ? redirect : '/dashboard')
}
} catch (err) {
this.error = 'An unexpected error occurred'
diff --git a/pages/onboarding/index.vue b/pages/onboarding/index.vue
new file mode 100644
index 0000000..4cbe347
--- /dev/null
+++ b/pages/onboarding/index.vue
@@ -0,0 +1,379 @@
+
+
+
+
+
+
+
+
+
+ {{ index + 1 }}
+
+
+
+
+
+
+
+
+
+ Create your workspace
+
+ A workspace is where your team collaborates on feedback and projects. You can think of it as your company or organization.
+
+
+
+
+
+
+
+
+
+
+
+ Invite your team
+
+ Veerify works best with your team. Add members now or invite them later from settings.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add another
+
+
+
+ {{ inviteError }}
+
+
+
+ {{ inviteSuccess }}
+
+
+
+
+
+ {{ isInviting ? 'Sending invitations...' : 'Send invitations' }}
+
+
+ Skip for now
+
+
+
+
+
+
+
+
+
+
+ You're all set!
+
+ Your workspace {{ createdOrgName }} is ready to go.
+
+
+
+
+
+
+
+
+
+
+
+
Collect feedback
+
Gather feature requests, bug reports, and ideas from your users.
+
+
+
+
+
+
+
+
Organize with projects
+
Create projects for different products or areas. Need more separation? Create additional teams later.
+
+
+
+
+
+
+
+
Collaborate with your team
+
Everyone in your workspace has access. Invite more members anytime from settings.
+
+
+
+
+
+ Go to dashboard
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/settings/index.vue b/pages/settings/index.vue
index cfad44f..f5984ea 100644
--- a/pages/settings/index.vue
+++ b/pages/settings/index.vue
@@ -52,8 +52,8 @@ const allTabs = [
{ key: 'profile', label: 'Profile', icon: 'lucide:user' },
{ key: 'security', label: 'Security', icon: 'lucide:shield' },
{ key: 'notifications', label: 'Notifications', icon: 'lucide:bell' },
- { key: 'organization', label: 'Organization', icon: 'lucide:building-2' },
- { key: 'team', label: 'Team', icon: 'lucide:users' },
+ { key: 'organization', label: 'Workspace', icon: 'lucide:building-2' },
+ { key: 'team', label: 'Teams', icon: 'lucide:users' },
{ key: 'billing', label: 'Billing', icon: 'lucide:credit-card' },
{ key: 'appearance', label: 'Appearance', icon: 'lucide:palette' },
]
@@ -83,11 +83,16 @@ export default {
return {
activeTab: 'profile',
currentOrgRole: null,
+ hasOrganization: false,
}
},
computed: {
availableTabs() {
return allTabs.filter((tab) => {
+ // Workspace-only tabs: hide when user has no org
+ if (['organization', 'team', 'billing'].includes(tab.key) && !this.hasOrganization) {
+ return false
+ }
if (tab.key === 'billing') {
return this.currentOrgRole === 'owner'
}
@@ -105,7 +110,7 @@ export default {
const validKeys = this.availableTabs.map((t) => t.key)
if (hash && validKeys.includes(hash)) {
this.activeTab = hash
- } else if (hash === 'billing' && !validKeys.includes('billing')) {
+ } else if (hash && !validKeys.includes(hash)) {
this.activeTab = 'profile'
window.history.replaceState(null, null, '#profile')
}
@@ -116,9 +121,12 @@ export default {
const activeOrg = await $fetch('/api/auth/organization/get-full-organization').catch(() => null)
if (!activeOrg?.id && !activeOrg?.name) {
this.currentOrgRole = null
+ this.hasOrganization = false
return
}
+ this.hasOrganization = true
+
let slug = activeOrg.slug || null
if (!slug) {
const listedOrgs = await $fetch('/api/auth/organization/list').catch(() => null)
diff --git a/pages/signup/index.vue b/pages/signup/index.vue
index 9be0048..7341590 100644
--- a/pages/signup/index.vue
+++ b/pages/signup/index.vue
@@ -75,7 +75,7 @@
Already have an account?
- Sign in
+ Sign in
@@ -115,6 +115,12 @@ export default {
error: '',
}
},
+ computed: {
+ loginLink() {
+ const redirect = this.$route.query.redirect
+ return redirect ? `/login?redirect=${encodeURIComponent(redirect)}` : '/login'
+ },
+ },
methods: {
async handleSubmit() {
if (!this.name || !this.email || !this.password) {
@@ -140,8 +146,8 @@ export default {
if (result.error) {
this.error = result.error.message || 'Sign up failed'
} else {
- // Success - redirect to dashboard
- await navigateTo('/dashboard')
+ const redirect = this.$route.query.redirect
+ await navigateTo(redirect && typeof redirect === 'string' && redirect.startsWith('/') ? redirect : '/dashboard')
}
} catch (err) {
this.error = 'An unexpected error occurred'
diff --git a/pages/submissions/index.vue b/pages/submissions/index.vue
new file mode 100644
index 0000000..b89e03a
--- /dev/null
+++ b/pages/submissions/index.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
My Submissions
+
All feedback you've submitted across projects
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Failed to load submissions
+ {{ error }}
+
+
+ Try again
+
+
+
+
+
+
+
+
+ No submissions yet
+
+ When you submit feedback on public boards, your submissions will appear here so you can track their progress.
+
+
+
+
+
+
+
+
+
+ {{ filter.label }}
+
+ {{ filter.count }}
+
+
+
+
+
+
+
+
+
{{ item.title }}
+
+ {{ item.body }}
+
+
+
+
+ {{ item.projectName || 'Unknown project' }}
+
+ {{ formatDate(item.createdAt) }}
+
+
+ {{ item.commentCount }}
+
+
+
+
+
+ {{ formatStatus(item.status) }}
+
+
+
+ {{ item.voteCount }}
+
+
+
+
+
+
+
+
+ Page {{ page }} of {{ totalPages }} ({{ totalCount }} submissions)
+
+
+
+ Previous
+
+
+ Next
+
+
+
+
+
+
+
+
+
diff --git a/server/api/user/submissions.get.ts b/server/api/user/submissions.get.ts
new file mode 100644
index 0000000..51108af
--- /dev/null
+++ b/server/api/user/submissions.get.ts
@@ -0,0 +1,59 @@
+import { auth } from '~/lib/auth'
+import { db } from '~/server/database/drizzle'
+import { feedback, project } from '~/server/database/schema/feedback'
+import { eq, desc, sql } from 'drizzle-orm'
+
+export default defineEventHandler(async (event) => {
+ const session = await auth.api.getSession({
+ headers: event.node.req.headers as any,
+ })
+ if (!session?.user) {
+ throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
+ }
+
+ const query = getQuery(event)
+ const page = Math.max(1, parseInt(String(query.page || '1'), 10))
+ const limit = Math.min(50, Math.max(1, parseInt(String(query.limit || '20'), 10)))
+ const offset = (page - 1) * limit
+
+ // Get all feedback submitted by this user across all projects
+ const [items, countResult] = await Promise.all([
+ db
+ .select({
+ id: feedback.id,
+ title: feedback.title,
+ body: feedback.body,
+ status: feedback.status,
+ voteCount: feedback.voteCount,
+ commentCount: feedback.commentCount,
+ createdAt: feedback.createdAt,
+ updatedAt: feedback.updatedAt,
+ projectId: feedback.projectId,
+ projectName: project.name,
+ projectSlug: project.slug,
+ })
+ .from(feedback)
+ .leftJoin(project, eq(feedback.projectId, project.id))
+ .where(eq(feedback.authorUserId, session.user.id))
+ .orderBy(desc(feedback.createdAt))
+ .limit(limit)
+ .offset(offset),
+ db
+ .select({ count: sql
`count(*)::int` })
+ .from(feedback)
+ .where(eq(feedback.authorUserId, session.user.id)),
+ ])
+
+ const totalCount = countResult[0]?.count || 0
+
+ return {
+ success: true,
+ data: items,
+ pagination: {
+ page,
+ limit,
+ totalCount,
+ totalPages: Math.ceil(totalCount / limit),
+ },
+ }
+})
diff --git a/server/database/schema/feedback.ts b/server/database/schema/feedback.ts
index 0ab4a98..bd292ee 100644
--- a/server/database/schema/feedback.ts
+++ b/server/database/schema/feedback.ts
@@ -19,6 +19,10 @@ export const project = pgTable(
isPublic: boolean('is_public')
.$defaultFn(() => false)
.notNull(),
+ // Controls who can submit feedback: 'anonymous' allows anonymous + optional email, 'account_required' requires login
+ submissionMode: text('submission_mode')
+ .$defaultFn(() => 'anonymous')
+ .notNull(),
customDomain: text('custom_domain'),
settings: jsonb('settings').$type>(),
createdAt: timestamp('created_at')