نسخ من mohammedsaid18/dashboard
connect to campaign graphql
هذا الالتزام موجود في:
9
package-lock.json
مولّد
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,16 +270,15 @@ 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">
|
||||||
@@ -138,39 +341,34 @@ const Settings = () => {
|
|||||||
View and update your personal details and account information.
|
View and update your personal details and account information.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form className="space-y-6">
|
{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,36 +376,23 @@ 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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -220,46 +405,55 @@ const Settings = () => {
|
|||||||
Manage your keyword campaigns and track performance.
|
Manage your keyword campaigns and track performance.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<button
|
||||||
Create Campaign
|
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>
|
</button>
|
||||||
</div>
|
</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,7 +462,14 @@ 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>
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم