Skip to content

Commit e9d9d95

Browse files
committed
feat(feedback): implement GitHub feedback issue builder and API client
1 parent dfe3c77 commit e9d9d95

16 files changed

Lines changed: 1324 additions & 1283 deletions

SortVision/src/app/globals.css

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,3 +471,117 @@
471471
.duration-300 {
472472
animation-duration: 300ms;
473473
}
474+
475+
@keyframes feedback-success-overlay-enter {
476+
from {
477+
opacity: 0;
478+
}
479+
to {
480+
opacity: 1;
481+
}
482+
}
483+
484+
@keyframes feedback-success-blob-drift {
485+
0%,
486+
100% {
487+
transform: translate(0, 0) scale(1);
488+
opacity: 0.28;
489+
}
490+
50% {
491+
transform: translate(2%, -3%) scale(1.05);
492+
opacity: 0.4;
493+
}
494+
}
495+
496+
@keyframes feedback-success-icon-pop {
497+
0% {
498+
transform: scale(0.88);
499+
opacity: 0;
500+
}
501+
100% {
502+
transform: scale(1);
503+
opacity: 1;
504+
}
505+
}
506+
507+
@keyframes feedback-success-ring-out {
508+
0% {
509+
transform: translate(-50%, -50%) scale(0.95);
510+
opacity: 0.55;
511+
}
512+
100% {
513+
transform: translate(-50%, -50%) scale(1.55);
514+
opacity: 0;
515+
}
516+
}
517+
518+
@keyframes feedback-success-content-up {
519+
0% {
520+
opacity: 0;
521+
transform: translateY(14px);
522+
}
523+
100% {
524+
opacity: 1;
525+
transform: translateY(0);
526+
}
527+
}
528+
529+
@keyframes feedback-success-sparkle-fade {
530+
0% {
531+
opacity: 0;
532+
}
533+
30% {
534+
opacity: 0.9;
535+
}
536+
100% {
537+
opacity: 0;
538+
}
539+
}
540+
541+
.feedback-success-overlay-enter {
542+
animation: feedback-success-overlay-enter 0.45s ease-out forwards;
543+
}
544+
545+
.feedback-success-blob {
546+
animation: feedback-success-blob-drift 10s ease-in-out infinite;
547+
}
548+
549+
.feedback-success-blob-delayed {
550+
animation: feedback-success-blob-drift 12s ease-in-out infinite;
551+
animation-delay: -4s;
552+
}
553+
554+
.feedback-success-icon-pop {
555+
animation: feedback-success-icon-pop 0.6s cubic-bezier(0.34, 1.25, 0.64, 1)
556+
both;
557+
}
558+
559+
.feedback-success-ring {
560+
animation: feedback-success-ring-out 1.1s cubic-bezier(0.22, 1, 0.36, 1)
561+
forwards;
562+
}
563+
564+
.feedback-success-content {
565+
animation: feedback-success-content-up 0.55s ease-out 0.35s both;
566+
}
567+
568+
.feedback-success-content-late {
569+
animation: feedback-success-content-up 0.5s ease-out 0.65s both;
570+
}
571+
572+
.feedback-success-sparkle {
573+
animation: feedback-success-sparkle-fade 1.05s ease-out forwards;
574+
}
575+
576+
@media (prefers-reduced-motion: reduce) {
577+
.feedback-success-overlay-enter,
578+
.feedback-success-blob,
579+
.feedback-success-blob-delayed,
580+
.feedback-success-icon-pop,
581+
.feedback-success-ring,
582+
.feedback-success-content,
583+
.feedback-success-content-late,
584+
.feedback-success-sparkle {
585+
animation: none;
586+
}
587+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { DEV_MODE } from './githubFeedbackConfig';
2+
import {
3+
getRatingDisplay,
4+
formatLocationInfo,
5+
formatLocationDetails,
6+
formatSessionData,
7+
formatDeviceInfo,
8+
formatNetworkInfo,
9+
formatPerformanceInfo,
10+
formatPageContext,
11+
formatMemoryInfo,
12+
formatAccessibilityInfo,
13+
formatFeatureUsage,
14+
formatErrorHistory,
15+
} from './githubIssueFormatters';
16+
17+
function getEmojiForType(type) {
18+
const emojiMap = {
19+
Bug: '🐛',
20+
'Feature Request': '✨',
21+
Suggestion: '💡',
22+
Other: '📝',
23+
};
24+
return emojiMap[type] || '📝';
25+
}
26+
27+
/**
28+
* Build GitHub issue title, markdown body, and labels from enriched feedback payload.
29+
*/
30+
export function buildGitHubFeedbackIssue(feedbackData) {
31+
const issueBody = `## Feedback Details
32+
33+
**👤 From:** ${feedbackData.name}
34+
**📧 Email:** ${feedbackData.email || 'Not provided (anonymous feedback)'}
35+
**⭐ Rating:** ${getRatingDisplay(feedbackData.rating)}
36+
**🌍 Region:** ${feedbackData.region || 'Not specified'}
37+
${formatLocationInfo(feedbackData.locationData)}
38+
**🏷️ Type:** ${feedbackData.feedbackType}
39+
**📬 Follow-up:** ${
40+
feedbackData.email
41+
? '✅ Email provided - can follow up'
42+
: '❌ Anonymous - no follow-up possible'
43+
}
44+
45+
---
46+
47+
## Detailed Feedback
48+
49+
${feedbackData.detailedFeedback}
50+
${formatLocationDetails(feedbackData.locationData)}
51+
${formatSessionData(feedbackData.sessionData)}
52+
${formatDeviceInfo(feedbackData.deviceInfo, feedbackData.browserCapabilities)}
53+
${formatNetworkInfo(feedbackData.networkInfo)}
54+
${formatPerformanceInfo(feedbackData.performanceInfo)}
55+
${formatPageContext(feedbackData.pageContext)}
56+
${formatMemoryInfo(feedbackData.memoryInfo)}
57+
${formatAccessibilityInfo(feedbackData.accessibilityInfo)}
58+
${formatFeatureUsage(feedbackData.featureUsage)}
59+
${formatErrorHistory(feedbackData.errorHistory)}
60+
61+
---
62+
63+
**🔧 Technical Metadata:**
64+
- **Source:** SortVision Feedback Form
65+
- **Environment:** ${DEV_MODE ? 'Development' : 'Production'}
66+
- **Session ID:** \`${feedbackData.sessionData?.sessionId || 'Unknown'}\`
67+
- **User Agent:** ${feedbackData.sessionData?.userAgent || navigator.userAgent}
68+
- **Page URL:** ${
69+
typeof window !== 'undefined' ? window.location.href : 'Unknown'
70+
}
71+
- **Submission ID:** ${Date.now().toString(36).toUpperCase()}`;
72+
73+
// Create labels based on feedback type
74+
const labels = [
75+
'user-feedback',
76+
feedbackData.feedbackType.toLowerCase().replace(/\s+/g, '-'),
77+
];
78+
79+
// Add priority label for bugs
80+
if (feedbackData.feedbackType === 'Bug') {
81+
labels.push('bug');
82+
}
83+
84+
// Add enhancement label for feature requests
85+
if (feedbackData.feedbackType === 'Feature Request') {
86+
labels.push('enhancement');
87+
}
88+
89+
// Add environment label
90+
if (DEV_MODE) {
91+
labels.push('development');
92+
}
93+
94+
return {
95+
title: `${getEmojiForType(feedbackData.feedbackType)} ${
96+
feedbackData.feedbackType
97+
}: ${feedbackData.name}`,
98+
body: issueBody,
99+
labels: labels,
100+
assignees: [],
101+
};
102+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Shared GitHub REST helpers for feedback tooling
3+
*/
4+
5+
import {
6+
GITHUB_API_BASE,
7+
REPO_OWNER,
8+
REPO_NAME,
9+
USER_AGENT,
10+
ENABLE_API_LOGGING,
11+
} from './githubFeedbackConfig';
12+
13+
export function githubHeaders(token) {
14+
return {
15+
Authorization: `token ${token}`,
16+
Accept: 'application/vnd.github.v3+json',
17+
'User-Agent': USER_AGENT,
18+
};
19+
}
20+
21+
/**
22+
* Validate GitHub token and repository access
23+
* @returns {Promise<boolean>} - Whether the token is valid
24+
*/
25+
export async function validateGitHubAccess() {
26+
const token = process.env.NEXT_PUBLIC_GITHUB_TOKEN;
27+
28+
if (!token || !REPO_OWNER) {
29+
return false;
30+
}
31+
32+
try {
33+
const response = await fetch(
34+
`${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}`,
35+
{
36+
headers: githubHeaders(token),
37+
}
38+
);
39+
40+
if (ENABLE_API_LOGGING) {
41+
console.log(
42+
'GitHub access validation:',
43+
response.ok ? 'Success' : 'Failed'
44+
);
45+
}
46+
47+
return response.ok;
48+
} catch (error) {
49+
if (ENABLE_API_LOGGING) {
50+
console.error('Error validating GitHub access:', error);
51+
}
52+
return false;
53+
}
54+
}
55+
56+
/**
57+
* Get repository information
58+
* @returns {Promise<Object|null>} - Repository information
59+
*/
60+
export async function getRepoInfo() {
61+
const token = process.env.NEXT_PUBLIC_GITHUB_TOKEN;
62+
63+
if (!token || !REPO_OWNER) {
64+
return null;
65+
}
66+
67+
try {
68+
const response = await fetch(
69+
`${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}`,
70+
{
71+
headers: githubHeaders(token),
72+
}
73+
);
74+
75+
if (response.ok) {
76+
const repoData = await response.json();
77+
if (ENABLE_API_LOGGING) {
78+
console.log('Repository info fetched:', {
79+
name: repoData.name,
80+
private: repoData.private,
81+
owner: repoData.owner.login,
82+
});
83+
}
84+
return repoData;
85+
}
86+
return null;
87+
} catch (error) {
88+
if (ENABLE_API_LOGGING) {
89+
console.error('Error fetching repository info:', error);
90+
}
91+
return null;
92+
}
93+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const GITHUB_API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
2+
export const REPO_OWNER = process.env.NEXT_PUBLIC_FEEDBACK_REPO_OWNER;
3+
export const REPO_NAME = process.env.NEXT_PUBLIC_FEEDBACK_REPO_NAME;
4+
export const USER_AGENT = process.env.NEXT_PUBLIC_API_USER_AGENT;
5+
export const DEV_MODE = process.env.NEXT_PUBLIC_DEV_MODE === 'true';
6+
export const ENABLE_API_LOGGING =
7+
process.env.NEXT_PUBLIC_ENABLE_API_LOGGING === 'true' || DEV_MODE;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export {
2+
getRatingDisplay,
3+
formatLocationInfo,
4+
formatLocationDetails,
5+
} from './githubIssueFormattersRatingLocation';
6+
7+
export {
8+
formatSessionData,
9+
formatDeviceInfo,
10+
formatNetworkInfo,
11+
formatPerformanceInfo,
12+
formatPageContext,
13+
} from './githubIssueFormattersEnvironment';
14+
15+
export {
16+
formatMemoryInfo,
17+
formatErrorHistory,
18+
formatFeatureUsage,
19+
formatAccessibilityInfo,
20+
} from './githubIssueFormattersExtended';

0 commit comments

Comments
 (0)