connect to campaign graphql

هذا الالتزام موجود في:
Your Name
2025-10-08 10:36:57 +00:00
الأصل a837154231
التزام 9230fd8e4e
3 ملفات معدلة مع 349 إضافات و138 حذوفات

9
package-lock.json مولّد
عرض الملف

@@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@apollo/client": "^4.0.7", "@apollo/client": "^4.0.7",
"@apollo/react-hooks": "^4.0.0",
"@nhost/react": "^3.11.2", "@nhost/react": "^3.11.2",
"graphql": "^16.11.0", "graphql": "^16.11.0",
"graphql-ws": "^6.0.6", "graphql-ws": "^6.0.6",
@@ -89,6 +90,14 @@
} }
} }
}, },
"node_modules/@apollo/react-hooks": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@apollo/react-hooks/-/react-hooks-4.0.0.tgz",
"integrity": "sha512-fCu0cbne3gbUl0QbA8X4L33iuuFVQbC5Jo2MIKRK8CyawR6PoxDpFdFA1kc6033ODZuZZ9Eo4RdeJFlFIIYcLA==",
"dependencies": {
"@apollo/client": "latest"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",

عرض الملف

@@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^4.0.7", "@apollo/client": "^4.0.7",
"@apollo/react-hooks": "^4.0.0",
"@nhost/react": "^3.11.2", "@nhost/react": "^3.11.2",
"graphql": "^16.11.0", "graphql": "^16.11.0",
"graphql-ws": "^6.0.6", "graphql-ws": "^6.0.6",

عرض الملف

@@ -1,8 +1,212 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { gql } from '@apollo/client';
import { useQuery, useMutation } from '@apollo/client/react';
import { useUserData, useAuthenticationStatus } from '@nhost/react';
// GraphQL Queries and Mutations
const GET_USER_PROFILE = gql`
query GetUserProfile{
user_profiles{
id
display_name
created_at
metadata
}
}
`;
const UPDATE_USER_PROFILE = gql`
mutation UpdateUserProfile($userId: uuid!, $displayName: String!, $metadata: jsonb) {
update_user_profiles_by_pk(
pk_columns: { id: $userId }
_set: { display_name: $displayName, metadata: $metadata }
) {
id
display_name
metadata
}
}
`;
const GET_USER_CAMPAIGNS = gql`
query GetUserCampaigns($userId: uuid!) {
campaigns(where: { user_id: { _eq: $userId } }) {
id
name
status
keywords_count
performance_metrics
created_at
updated_at
}
}
`;
const CREATE_CAMPAIGN = gql`
mutation CreateCampaign($userId: uuid!, $name: String!, $keywords: jsonb) {
insert_campaigns_one(object: {
user_id: $userId,
name: $name,
keywords: $keywords,
status: "draft",
keywords_count: 0
}) {
id
name
status
created_at
}
}
`;
const UPDATE_CAMPAIGN = gql`
mutation UpdateCampaign($campaignId: uuid!, $name: String, $status: String) {
update_campaigns_by_pk(
pk_columns: { id: $campaignId }
_set: { name: $name, status: $status }
) {
id
name
status
updated_at
}
}
`;
const DELETE_CAMPAIGN = gql`
mutation DeleteCampaign($campaignId: uuid!) {
delete_campaigns_by_pk(id: $campaignId) {
id
name
}
}
`;
const Settings = () => { const Settings = () => {
const [activeSection, setActiveSection] = useState("profile"); const [activeSection, setActiveSection] = useState("profile");
const [activeSubSection, setActiveSubSection] = useState("payment-history"); const [activeSubSection, setActiveSubSection] = useState("payment-history");
const [formData, setFormData] = useState({
displayName: '',
email: ''
});
// NHost Authentication
const user = useUserData();
const { isAuthenticated } = useAuthenticationStatus();
// GraphQL Queries
const { data: profileData, loading: profileLoading, refetch: refetchProfile } = useQuery(GET_USER_PROFILE, {
variables: { userId: user?.id },
skip: !user?.id
});
const { data: campaignsData, loading: campaignsLoading, refetch: refetchCampaigns } = useQuery(GET_USER_CAMPAIGNS, {
variables: { userId: user?.id },
skip: !user?.id
});
// GraphQL Mutations
const [updateUserProfile, { loading: updatingProfile }] = useMutation(UPDATE_USER_PROFILE);
const [createCampaign, { loading: creatingCampaign }] = useMutation(CREATE_CAMPAIGN);
const [updateCampaign] = useMutation(UPDATE_CAMPAIGN);
const [deleteCampaign] = useMutation(DELETE_CAMPAIGN);
// Initialize form data when profile loads
useEffect(() => {
if (profileData?.user_profiles) {
const userProfile = profileData.user_profiles[0];
setFormData({
displayName: userProfile.display_name || '',
email: userProfile.email || ''
});
}
}, [profileData]);
const handleSectionClick = (sectionId: string) => {
setActiveSection(sectionId);
if (sectionId === "billing") {
setActiveSubSection("payment-history");
}
};
const handleSubSectionClick = (subSection: string) => {
setActiveSubSection(subSection);
};
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const handleSaveProfile = async (e: React.FormEvent) => {
e.preventDefault();
if (!user?.id) return;
try {
await updateUserProfile({
variables: {
userId: user.id,
displayName: formData.displayName,
metadata: { last_updated: new Date().toISOString() }
}
});
await refetchProfile();
// You can add a toast notification here for success
} catch (error) {
console.error('Error updating profile:', error);
// Handle error (show toast, etc.)
}
};
const handleCreateCampaign = async () => {
if (!user?.id) return;
try {
await createCampaign({
variables: {
userId: user.id,
name: `New Campaign ${new Date().toLocaleDateString()}`,
keywords: []
}
});
await refetchCampaigns();
} catch (error) {
console.error('Error creating campaign:', error);
}
};
const handleUpdateCampaign = async (campaignId: string, updates: { name?: string; status?: string }) => {
try {
await updateCampaign({
variables: {
campaignId,
...updates
}
});
await refetchCampaigns();
} catch (error) {
console.error('Error updating campaign:', error);
}
};
const handleDeleteCampaign = async (campaignId: string) => {
if (!confirm('Are you sure you want to delete this campaign?')) return;
try {
await deleteCampaign({
variables: { campaignId }
});
await refetchCampaigns();
} catch (error) {
console.error('Error deleting campaign:', error);
}
};
const sidebarItems = [ const sidebarItems = [
{ {
@@ -66,111 +270,105 @@ const Settings = () => {
} }
]; ];
const handleSectionClick = (sectionId: string) => { if (!isAuthenticated) {
setActiveSection(sectionId); return (
if (sectionId === "billing") { <div className="max-w-7xl mx-auto px-4 sm:px-6 py-8">
setActiveSubSection("payment-history"); <div className="text-center">
} <h2 className="text-2xl font-bold text-gray-900">Please log in to access settings</h2>
}; </div>
</div>
const handleSubSectionClick = (subSection: string) => { );
setActiveSubSection(subSection); }
};
return ( return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 py-8">
<div className="flex flex-col lg:flex-row gap-8"> <div className="flex flex-col lg:flex-row gap-8">
{/* Sidebar */} {/* Sidebar */}
<div className="w-full lg:w-64 flex-shrink-0"> <div className="w-full lg:w-64 flex-shrink-0">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-6 lg:mb-8">Settings</h1> <h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-6 lg:mb-8">Settings</h1>
<nav className="space-y-2"> <nav className="space-y-2">
{sidebarItems.map((item) => ( {sidebarItems.map((item) => (
<div key={item.id}> <div key={item.id}>
<button <button
onClick={() => handleSectionClick(item.id)} onClick={() => handleSectionClick(item.id)}
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg text-left transition-colors ${ className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg text-left transition-colors ${
activeSection === item.id activeSection === item.id
? "bg-blue-50 text-blue-600" ? "bg-blue-50 text-blue-600"
: "text-gray-700 hover:bg-gray-50" : "text-gray-700 hover:bg-gray-50"
}`} }`}
> >
{item.icon} {item.icon}
<span className="font-medium">{item.label}</span> <span className="font-medium">{item.label}</span>
{item.hasSubItems && ( {item.hasSubItems && (
<svg className="w-4 h-4 ml-auto" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-4 h-4 ml-auto" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" /> <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg> </svg>
)}
</button>
{item.hasSubItems && activeSection === item.id && (
<div className="ml-8 mt-2 space-y-1">
{item.subItems?.map((subItem) => (
<button
key={subItem}
onClick={() => handleSubSectionClick(subItem.toLowerCase().replace(/\s+/g, "-"))}
className={`w-full flex items-center px-4 py-2 rounded-lg text-left transition-colors ${
activeSubSection === subItem.toLowerCase().replace(/\s+/g, "-")
? "bg-blue-600 text-white"
: "text-gray-600 hover:bg-gray-50"
}`}
>
{activeSubSection === subItem.toLowerCase().replace(/\s+/g, "-") && (
<div className="w-1 h-6 bg-blue-600 rounded-full mr-3"></div>
)}
<span className="text-sm">{subItem}</span>
</button>
))}
</div>
)} )}
</div> </button>
))}
</nav>
</div>
{/* Main Content */} {item.hasSubItems && activeSection === item.id && (
<div className="flex-1"> <div className="ml-8 mt-2 space-y-1">
{activeSection === "profile" && ( {item.subItems?.map((subItem) => (
<div className="bg-white"> <button
<h2 className="text-3xl font-bold text-gray-900 mb-2">Basic Information</h2> key={subItem}
<p className="text-gray-600 mb-8"> onClick={() => handleSubSectionClick(subItem.toLowerCase().replace(/\s+/g, "-"))}
View and update your personal details and account information. className={`w-full flex items-center px-4 py-2 rounded-lg text-left transition-colors ${
</p> activeSubSection === subItem.toLowerCase().replace(/\s+/g, "-")
? "bg-blue-600 text-white"
: "text-gray-600 hover:bg-gray-50"
}`}
>
{activeSubSection === subItem.toLowerCase().replace(/\s+/g, "-") && (
<div className="w-1 h-6 bg-blue-600 rounded-full mr-3"></div>
)}
<span className="text-sm">{subItem}</span>
</button>
))}
</div>
)}
</div>
))}
</nav>
</div>
<form className="space-y-6"> {/* Main Content */}
<div className="flex-1">
{activeSection === "profile" && (
<div className="bg-white">
<h2 className="text-3xl font-bold text-gray-900 mb-2">Basic Information</h2>
<p className="text-gray-600 mb-8">
View and update your personal details and account information.
</p>
{profileLoading ? (
<div className="text-center py-8">Loading profile...</div>
) : (
<form onSubmit={handleSaveProfile} className="space-y-6">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Name Name
</label> </label>
<input <input
type="text" type="text"
defaultValue="Eugiene Jonathan" value={formData.displayName}
onChange={(e) => handleInputChange('displayName', e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/> />
</div> </div>
<div> {/* <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Email Email
</label> </label>
<input <input
type="email" type="email"
defaultValue="Eugiene Jonathan" value={formData.email}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" disabled
className="w-full px-4 py-3 border border-gray-300 rounded-lg bg-gray-50"
/> />
</div> <p className="text-sm text-gray-500 mt-1">Email cannot be changed</p>
</div> */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Password
</label>
<input
type="password"
defaultValue="Eugiene Jonathan"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
@@ -178,88 +376,84 @@ const Settings = () => {
</label> </label>
<input <input
type="text" type="text"
defaultValue="12 Aug 2025" value={new Date(profileData?.user_profiles[0]?.created_at).toLocaleDateString() || 'N/A'}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" disabled
/> className="w-full px-4 py-3 border border-gray-300 rounded-lg bg-gray-50"
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<div className="flex items-center space-x-2">
<span>Total Keywords Campaigns</span>
<svg className="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
</svg>
</div>
</label>
<input
type="text"
defaultValue="234,029"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/> />
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
type="submit" type="submit"
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors" disabled={updatingProfile}
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
> >
Save {updatingProfile ? 'Saving...' : 'Save'}
</button> </button>
</div> </div>
</form> </form>
</div> )}
)} </div>
)}
{activeSection === "campaigns" && ( {activeSection === "campaigns" && (
<div className="bg-white"> <div className="bg-white">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div> <div>
<h2 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">Campaigns</h2> <h2 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">Campaigns</h2>
<p className="text-sm sm:text-base text-gray-600"> <p className="text-sm sm:text-base text-gray-600">
Manage your keyword campaigns and track performance. Manage your keyword campaigns and track performance.
</p> </p>
</div>
<button className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors whitespace-nowrap self-start sm:self-auto">
Create Campaign
</button>
</div> </div>
<button
onClick={handleCreateCampaign}
disabled={creatingCampaign}
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors whitespace-nowrap self-start sm:self-auto disabled:opacity-50"
>
{creatingCampaign ? 'Creating...' : 'Create Campaign'}
</button>
</div>
{campaignsLoading ? (
<div className="text-center py-8">Loading campaigns...</div>
) : (
<div className="space-y-4"> <div className="space-y-4">
{[ {campaignsData?.campaigns?.map((campaign: any) => (
{ name: "Summer Sale 2025", keywords: 127, status: "Active", performance: "+12.5%" }, <div key={campaign.id} className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:border-blue-300 transition-colors">
{ name: "Product Launch Q3", keywords: 89, status: "Active", performance: "+8.3%" },
{ name: "Brand Awareness", keywords: 234, status: "Paused", performance: "+5.2%" },
{ name: "Holiday Campaign", keywords: 156, status: "Active", performance: "+15.7%" },
].map((campaign, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:border-blue-300 transition-colors">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex-1"> <div className="flex-1">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 mb-2"> <div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 mb-2">
<h3 className="text-base sm:text-lg font-semibold text-gray-900">{campaign.name}</h3> <h3 className="text-base sm:text-lg font-semibold text-gray-900">{campaign.name}</h3>
<span className={`px-3 py-1 rounded-full text-xs font-medium self-start ${ <span className={`px-3 py-1 rounded-full text-xs font-medium self-start ${
campaign.status === "Active" campaign.status === "active"
? "bg-green-100 text-green-700" ? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700" : campaign.status === "paused"
? "bg-yellow-100 text-yellow-700"
: "bg-gray-100 text-gray-700"
}`}> }`}>
{campaign.status} {campaign.status}
</span> </span>
</div> </div>
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-6 text-sm text-gray-600"> <div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-6 text-sm text-gray-600">
<span>{campaign.keywords} keywords</span> <span>{campaign.keywords_count || 0} keywords</span>
<span className="flex items-center space-x-1"> <span>Created: {new Date(campaign.created_at).toLocaleDateString()}</span>
<span>Performance:</span>
<span className="text-green-600 font-medium">{campaign.performance}</span>
</span>
</div> </div>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button className="p-2 text-gray-400 hover:text-blue-600 transition-colors"> <button
onClick={() => handleUpdateCampaign(campaign.id, {
name: prompt('Enter new campaign name:', campaign.name) || campaign.name
})}
className="p-2 text-gray-400 hover:text-blue-600 transition-colors"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" /> <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg> </svg>
</button> </button>
<button className="p-2 text-gray-400 hover:text-red-600 transition-colors"> <button
onClick={() => handleDeleteCampaign(campaign.id)}
className="p-2 text-gray-400 hover:text-red-600 transition-colors"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" /> <path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
</svg> </svg>
@@ -268,12 +462,19 @@ const Settings = () => {
</div> </div>
</div> </div>
))} ))}
{(!campaignsData?.campaigns || campaignsData.campaigns.length === 0) && (
<div className="text-center py-8 text-gray-500">
No campaigns found. Create your first campaign!
</div>
)}
</div> </div>
</div> )}
)} </div>
</div> )}
</div> </div>
</div> </div>
</div>
); );
}; };