How to Update a Kanban List Dynamically After Creating a New Task with MongoDB?

‘m working on a Kanban application where I need the list of tasks to update immediately after creating a new task. I’m using MongoDB with Express and Axios, but I’m having trouble getting the list to refresh dynamically without a full page reload.

Here are the main components of my application:

1.List Component: This component fetches and displays tasks grouped by stages. It uses the useState and useEffect hooks to manage state and fetch data from the server.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>type Task = {
id: string;
title: string;
description: string;
dueDate?: Date;
completed: boolean;
stageId?: string | null;
users: { id: string; name: string; avatarUrl: string }[];
createdAt: string;
updatedAt: string;
};
type TaskStage = {
id: string;
title: string;
tasks: Task[];
createdAt: string;
updatedAt: string;
};
const List = ({ children }: React.PropsWithChildren) => {
const [tasks, setTasks] = useState<Task[]>([]);
const [stages, setStages] = useState<TaskStage[]>([]);
const [isLoading, setIsLoading] = useState(true);
const {replace} = useNavigation();
// Fetch tasks and stages from MongoDB
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get('http://localhost:3000/');
setTasks(response.data.tasks);
setStages(response.data.taskStages);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
// console.log(tasks);
// console.log(stages);
// Group tasks by stages
const tasksStages = React.useMemo(() => {
if (!tasks.length || !stages.length) {
return {
unassignedStage: [],
columns: []
};
}
const unassignedStage = tasks.filter((task: Task) => !task.stageId) ?? [];
const groupedStages = stages.map((stage: TaskStage) => ({
...stage,
tasks: tasks.filter((task: Task) => task.stageId === stage.id) ?? [],
})) ?? [];
return {
unassignedStage,
columns: groupedStages
};
}, [stages, tasks]);
const handleTaskCreate = (newTask: Task) => {
setTasks((prevTasks) => [...prevTasks, newTask]);
};
// Handle adding new card by pressing the pluss icon :
const handleAddCard = (args: { stageId: string }) => {
const path = args.stageId === 'unassigned' ? '/tasks/new' : `/tasks/new?stageId=${args.stageId}`;
replace(path); // Use navigate instead of replace
};
...
return (
<>
<KanbanBoardContainer>
<KanbanBoard onDragEnd={handleOnDragEnd}>
<KanbanColumn
id="unassigned"
title="Unassigned"
count={tasksStages.unassignedStage.length || 0}
onAddClick={() => handleAddCard({ stageId: 'unassigned' })} description={undefined}>
{tasksStages.unassignedStage.map((task: Task) => (
<KanbanItem key={task.id} id={task.id} data={{ ...task, stageId: 'unassigned' }}>
<ProjectCardMemo {...task} dueDate={String(task.dueDate) || undefined} />
</KanbanItem>
))}
{!tasksStages.unassignedStage.length && <KanbanAddCardButton onClick={() => handleAddCard({ stageId: 'unassigned' })} />}
</KanbanColumn>
{tasksStages.columns?.map((column: TaskStage) => (
<KanbanColumn
key={column.id}
id={column.id}
title={column.title}
count={column.tasks.length}
onAddClick={() => handleAddCard({ stageId: column.id })} description={undefined}>
{column.tasks.map((task: Task) => (
<KanbanItem key={task.id} id={task.id} data={task}>
<ProjectCardMemo {...task} dueDate={String(task.dueDate) || undefined} />
</KanbanItem>
))}
{!column.tasks.length && <KanbanAddCardButton onClick={() => handleAddCard({ stageId: column.id })} />}
</KanbanColumn>
))}
</KanbanBoard>
</KanbanBoardContainer>
{children}
</>
);
};
export default List;
</code>
<code>type Task = { id: string; title: string; description: string; dueDate?: Date; completed: boolean; stageId?: string | null; users: { id: string; name: string; avatarUrl: string }[]; createdAt: string; updatedAt: string; }; type TaskStage = { id: string; title: string; tasks: Task[]; createdAt: string; updatedAt: string; }; const List = ({ children }: React.PropsWithChildren) => { const [tasks, setTasks] = useState<Task[]>([]); const [stages, setStages] = useState<TaskStage[]>([]); const [isLoading, setIsLoading] = useState(true); const {replace} = useNavigation(); // Fetch tasks and stages from MongoDB useEffect(() => { const fetchData = async () => { try { const response = await axios.get('http://localhost:3000/'); setTasks(response.data.tasks); setStages(response.data.taskStages); } catch (error) { console.error('Error fetching data:', error); } finally { setIsLoading(false); } }; fetchData(); }, []); // console.log(tasks); // console.log(stages); // Group tasks by stages const tasksStages = React.useMemo(() => { if (!tasks.length || !stages.length) { return { unassignedStage: [], columns: [] }; } const unassignedStage = tasks.filter((task: Task) => !task.stageId) ?? []; const groupedStages = stages.map((stage: TaskStage) => ({ ...stage, tasks: tasks.filter((task: Task) => task.stageId === stage.id) ?? [], })) ?? []; return { unassignedStage, columns: groupedStages }; }, [stages, tasks]); const handleTaskCreate = (newTask: Task) => { setTasks((prevTasks) => [...prevTasks, newTask]); }; // Handle adding new card by pressing the pluss icon : const handleAddCard = (args: { stageId: string }) => { const path = args.stageId === 'unassigned' ? '/tasks/new' : `/tasks/new?stageId=${args.stageId}`; replace(path); // Use navigate instead of replace }; ... return ( <> <KanbanBoardContainer> <KanbanBoard onDragEnd={handleOnDragEnd}> <KanbanColumn id="unassigned" title="Unassigned" count={tasksStages.unassignedStage.length || 0} onAddClick={() => handleAddCard({ stageId: 'unassigned' })} description={undefined}> {tasksStages.unassignedStage.map((task: Task) => ( <KanbanItem key={task.id} id={task.id} data={{ ...task, stageId: 'unassigned' }}> <ProjectCardMemo {...task} dueDate={String(task.dueDate) || undefined} /> </KanbanItem> ))} {!tasksStages.unassignedStage.length && <KanbanAddCardButton onClick={() => handleAddCard({ stageId: 'unassigned' })} />} </KanbanColumn> {tasksStages.columns?.map((column: TaskStage) => ( <KanbanColumn key={column.id} id={column.id} title={column.title} count={column.tasks.length} onAddClick={() => handleAddCard({ stageId: column.id })} description={undefined}> {column.tasks.map((task: Task) => ( <KanbanItem key={task.id} id={task.id} data={task}> <ProjectCardMemo {...task} dueDate={String(task.dueDate) || undefined} /> </KanbanItem> ))} {!column.tasks.length && <KanbanAddCardButton onClick={() => handleAddCard({ stageId: column.id })} />} </KanbanColumn> ))} </KanbanBoard> </KanbanBoardContainer> {children} </> ); }; export default List; </code>
type Task = {
  id: string;
  title: string;
  description: string;
  dueDate?: Date;
  completed: boolean;
  stageId?: string | null;
  users: { id: string; name: string; avatarUrl: string }[];
  createdAt: string;
  updatedAt: string;
};

type TaskStage = {
  id: string;
  title: string;
  tasks: Task[];
  createdAt: string;
  updatedAt: string;
};

const List = ({ children }: React.PropsWithChildren) => {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [stages, setStages] = useState<TaskStage[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const {replace} = useNavigation();

  // Fetch tasks and stages from MongoDB
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('http://localhost:3000/');
        setTasks(response.data.tasks);
        setStages(response.data.taskStages);
      } catch (error) {
        console.error('Error fetching data:', error);
      } finally {
        setIsLoading(false);
      }
    };
    fetchData();
  }, []);
  // console.log(tasks);
  // console.log(stages);
 
  // Group tasks by stages
  const tasksStages = React.useMemo(() => {
    if (!tasks.length || !stages.length) {
      return {
        unassignedStage: [],
        columns: []
      };
    }

    const unassignedStage = tasks.filter((task: Task) => !task.stageId) ?? [];
    const groupedStages = stages.map((stage: TaskStage) => ({
      ...stage,
      tasks: tasks.filter((task: Task) => task.stageId === stage.id) ?? [],
    })) ?? [];

    return {
      unassignedStage,
      columns: groupedStages
    };
  }, [stages, tasks]);
  
  const handleTaskCreate = (newTask: Task) => {
    setTasks((prevTasks) => [...prevTasks, newTask]);
};
  // Handle adding new card by pressing the pluss icon : 
  const handleAddCard = (args: { stageId: string }) => {
    const path = args.stageId === 'unassigned' ? '/tasks/new' : `/tasks/new?stageId=${args.stageId}`;
    replace(path); // Use navigate instead of replace
  };
...
 return (
    <>
      <KanbanBoardContainer>
        <KanbanBoard onDragEnd={handleOnDragEnd}>
          <KanbanColumn
            id="unassigned"
            title="Unassigned"
            count={tasksStages.unassignedStage.length || 0}
            onAddClick={() => handleAddCard({ stageId: 'unassigned' })} description={undefined}>
            {tasksStages.unassignedStage.map((task: Task) => (
              <KanbanItem key={task.id} id={task.id} data={{ ...task, stageId: 'unassigned' }}>
                <ProjectCardMemo {...task} dueDate={String(task.dueDate) || undefined} />
              </KanbanItem>
            ))}
            {!tasksStages.unassignedStage.length && <KanbanAddCardButton onClick={() => handleAddCard({ stageId: 'unassigned' })} />}
          </KanbanColumn>

          {tasksStages.columns?.map((column: TaskStage) => (
            <KanbanColumn
              key={column.id}
              id={column.id}
              title={column.title}
              count={column.tasks.length}
              onAddClick={() => handleAddCard({ stageId: column.id })} description={undefined}>
              {column.tasks.map((task: Task) => (
                <KanbanItem key={task.id} id={task.id} data={task}>
                  <ProjectCardMemo {...task} dueDate={String(task.dueDate) || undefined} />
                </KanbanItem>
              ))}
              {!column.tasks.length && <KanbanAddCardButton onClick={() => handleAddCard({ stageId: column.id })} />}
            </KanbanColumn>
          ))}
        </KanbanBoard>
      </KanbanBoardContainer>
      {children}
    </>
  );
};

export default List;

2.TasksCreatePage Component: This component is used to create new tasks. It opens a modal form where users can input task details. Upon submission, the task is created, but I need to find a way to ensure the new task appears in the list immediately.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>import { useSearchParams } from "react-router-dom";
import { useModalForm } from "@refinedev/antd";
import { useNavigation } from "@refinedev/core";
import { Form, Input, Modal } from "antd";
import axios from "axios";
const CreateTask = () => {
const [searchParams] = useSearchParams();
const { list } = useNavigation();
const { formProps, modalProps, close } = useModalForm({
action: "create",
defaultVisible: true,
});
const onSubmit = async (values: any) => {
const stageId = searchParams.get("stageId") || null;
try {
const response = await axios.post('http://localhost:3000/tasks', {
title: values.title,
stageId,
completed: false,
users: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
if (response.status === 201) {
// Optionally show a success message here
list("tasks", "replace");
}
} catch (error) {
console.error('Error creating task:', error);
}
close();
};
return (
<Modal
{...modalProps}
onCancel={() => {
close();
list("tasks", "replace");
}}
title="Add new card"
width={512}
>
<Form {...formProps} layout="vertical" onFinish={onSubmit}>
<Form.Item label="Title" name="title" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
</Modal>
);
};
export default CreateTask;
</code>
<code>import { useSearchParams } from "react-router-dom"; import { useModalForm } from "@refinedev/antd"; import { useNavigation } from "@refinedev/core"; import { Form, Input, Modal } from "antd"; import axios from "axios"; const CreateTask = () => { const [searchParams] = useSearchParams(); const { list } = useNavigation(); const { formProps, modalProps, close } = useModalForm({ action: "create", defaultVisible: true, }); const onSubmit = async (values: any) => { const stageId = searchParams.get("stageId") || null; try { const response = await axios.post('http://localhost:3000/tasks', { title: values.title, stageId, completed: false, users: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); if (response.status === 201) { // Optionally show a success message here list("tasks", "replace"); } } catch (error) { console.error('Error creating task:', error); } close(); }; return ( <Modal {...modalProps} onCancel={() => { close(); list("tasks", "replace"); }} title="Add new card" width={512} > <Form {...formProps} layout="vertical" onFinish={onSubmit}> <Form.Item label="Title" name="title" rules={[{ required: true }]}> <Input /> </Form.Item> </Form> </Modal> ); }; export default CreateTask; </code>
import { useSearchParams } from "react-router-dom";
import { useModalForm } from "@refinedev/antd";
import { useNavigation } from "@refinedev/core";
import { Form, Input, Modal } from "antd";
import axios from "axios";

const CreateTask = () => {
  const [searchParams] = useSearchParams();
  const { list } = useNavigation();

  const { formProps, modalProps, close } = useModalForm({
    action: "create",
    defaultVisible: true,
  });

  const onSubmit = async (values: any) => {
    const stageId = searchParams.get("stageId") || null;

    try {
      const response = await axios.post('http://localhost:3000/tasks', {
        title: values.title,
        stageId,
        completed: false,
        users: [],
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      });

      if (response.status === 201) {
        // Optionally show a success message here
        list("tasks", "replace");
      }
    } catch (error) {
      console.error('Error creating task:', error);
    }

    close();
  };

  return (
    <Modal
      {...modalProps}
      onCancel={() => {
        close();
        list("tasks", "replace");
      }}
      title="Add new card"
      width={512}
    >
      <Form {...formProps} layout="vertical" onFinish={onSubmit}>
        <Form.Item label="Title" name="title" rules={[{ required: true }]}>
          <Input />
        </Form.Item>
      </Form>
    </Modal>
  );
};

export default CreateTask;

3.App Component: This component manages routing in the application and includes a route to TasksCreatePage.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code> <Route path="/tasks" element={<List>
<Outlet/>
</List>}>
<Route path="new" element={<CreateTask/>}/>
<Route path="edit/:id" element={<EditTask/>}/>
</Route>
</code>
<code> <Route path="/tasks" element={<List> <Outlet/> </List>}> <Route path="new" element={<CreateTask/>}/> <Route path="edit/:id" element={<EditTask/>}/> </Route> </code>
 <Route path="/tasks" element={<List>
                          <Outlet/>
                        </List>}>
                          <Route path="new" element={<CreateTask/>}/>
                          <Route path="edit/:id" element={<EditTask/>}/>
                        </Route>

How can I update the List component dynamically when a new task is created using MongoDB?
I would appreciate any guidance or examples on how to achieve this functionality in a React application.

From my understanding, a solution that would work for this would be to use a Context Provider. https://react.dev/reference/react/useContext

You can create a TaskContext which will allow tasks and stages to be globally available.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>import React, { createContext, useContext, useState } from 'react';
// Create a context for task-related actions
const TaskContext = createContext(null);
export const useTaskContext = () => useContext(TaskContext);
// Create a provider for tasks and stages
export const TaskProvider = ({ children }) => {
const [tasks, setTasks] = useState<Task[]>([]);
const [stages, setStages] = useState<TaskStage[]>([]);
const handleTaskCreate = (newTask: Task) => {
setTasks((prevTasks) => [...prevTasks, newTask]);
};
return (
<TaskContext.Provider value={{ tasks, stages, handleTaskCreate }}>
{children}
</TaskContext.Provider>
);
};
</code>
<code>import React, { createContext, useContext, useState } from 'react'; // Create a context for task-related actions const TaskContext = createContext(null); export const useTaskContext = () => useContext(TaskContext); // Create a provider for tasks and stages export const TaskProvider = ({ children }) => { const [tasks, setTasks] = useState<Task[]>([]); const [stages, setStages] = useState<TaskStage[]>([]); const handleTaskCreate = (newTask: Task) => { setTasks((prevTasks) => [...prevTasks, newTask]); }; return ( <TaskContext.Provider value={{ tasks, stages, handleTaskCreate }}> {children} </TaskContext.Provider> ); }; </code>
import React, { createContext, useContext, useState } from 'react';

// Create a context for task-related actions
const TaskContext = createContext(null);

export const useTaskContext = () => useContext(TaskContext);

// Create a provider for tasks and stages
export const TaskProvider = ({ children }) => {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [stages, setStages] = useState<TaskStage[]>([]);

  const handleTaskCreate = (newTask: Task) => {
    setTasks((prevTasks) => [...prevTasks, newTask]);
  };

  return (
    <TaskContext.Provider value={{ tasks, stages, handleTaskCreate }}>
      {children}
    </TaskContext.Provider>
  );
};

You can then update your List component and CreateTask component to utilize the TaskContext, and update dependency array for useEffect

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>import { useTaskContext } from './TaskContext';
const List = ({ children }) => {
const { tasks, setTasks, stages, setStages } = useTaskContext();
// Fetch tasks and stages from MongoDB
useEffect(() => {
const fetchData = async () => {
const { data } = await axios.get('http://localhost:3000/');
setTasks(data.tasks);
setStages(data.taskStages);
};
fetchData();
}, [setTasks, setStages]);
// Group tasks by stages
const tasksStages = useMemo(() => {
const unassignedStage = tasks.filter(task => !task.stageId);
const groupedStages = stages.map(stage => ({
...stage,
tasks: tasks.filter(task => task.stageId === stage.id),
}));
return { unassignedStage, columns: groupedStages };
}, [tasks, stages]);
return (
<>
/* Kanban Board */
{children}
</>
);
};
</code>
<code>import { useTaskContext } from './TaskContext'; const List = ({ children }) => { const { tasks, setTasks, stages, setStages } = useTaskContext(); // Fetch tasks and stages from MongoDB useEffect(() => { const fetchData = async () => { const { data } = await axios.get('http://localhost:3000/'); setTasks(data.tasks); setStages(data.taskStages); }; fetchData(); }, [setTasks, setStages]); // Group tasks by stages const tasksStages = useMemo(() => { const unassignedStage = tasks.filter(task => !task.stageId); const groupedStages = stages.map(stage => ({ ...stage, tasks: tasks.filter(task => task.stageId === stage.id), })); return { unassignedStage, columns: groupedStages }; }, [tasks, stages]); return ( <> /* Kanban Board */ {children} </> ); }; </code>
import { useTaskContext } from './TaskContext';

const List = ({ children }) => {
  const { tasks, setTasks, stages, setStages } = useTaskContext();

  // Fetch tasks and stages from MongoDB
  useEffect(() => {
    const fetchData = async () => {
      const { data } = await axios.get('http://localhost:3000/');
      setTasks(data.tasks);
      setStages(data.taskStages);
    };
    fetchData();
  }, [setTasks, setStages]);

  // Group tasks by stages
  const tasksStages = useMemo(() => {
    const unassignedStage = tasks.filter(task => !task.stageId);
    const groupedStages = stages.map(stage => ({
      ...stage,
      tasks: tasks.filter(task => task.stageId === stage.id),
    }));
    return { unassignedStage, columns: groupedStages };
  }, [tasks, stages]);

  return (
    <>
      /* Kanban Board */
      {children}
    </>
  );
};

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>import { useTaskContext } from './TaskContext';
const CreateTask = () => {
const { handleTaskCreate } = useTaskContext();
const [title, setTitle] = useState('');
const onSubmit = (e) => {
e.preventDefault();
const newTask = {
id: String(Date.now()),
title,
completed: false,
stageId: null,
};
handleTaskCreate(newTask);
};
return (
<form onSubmit={onSubmit}>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button type="submit">Create Task</button>
</form>
);
};
</code>
<code>import { useTaskContext } from './TaskContext'; const CreateTask = () => { const { handleTaskCreate } = useTaskContext(); const [title, setTitle] = useState(''); const onSubmit = (e) => { e.preventDefault(); const newTask = { id: String(Date.now()), title, completed: false, stageId: null, }; handleTaskCreate(newTask); }; return ( <form onSubmit={onSubmit}> <input value={title} onChange={(e) => setTitle(e.target.value)} /> <button type="submit">Create Task</button> </form> ); }; </code>
import { useTaskContext } from './TaskContext';

const CreateTask = () => {
  const { handleTaskCreate } = useTaskContext();
  const [title, setTitle] = useState('');

  const onSubmit = (e) => {
    e.preventDefault();
    const newTask = {
      id: String(Date.now()),
      title,
      completed: false,
      stageId: null,
    };
    handleTaskCreate(newTask);
  };

  return (
    <form onSubmit={onSubmit}>
      <input value={title} onChange={(e) => setTitle(e.target.value)} />
      <button type="submit">Create Task</button>
    </form>
  );
};

And finally update App.js and wrap your app with TaskContext to make it acessable to all components.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>{/* previous imports */}
import { TaskProvider } from './TaskContext'; // Import TaskProvider
function App() {
return (
<Routes>
<Route
path="/tasks"
element={
// Wrap TaskProvider around List and its nested routes
<TaskProvider>
<List>
<Outlet />
</List>
</TaskProvider>
}
>
<Route path="new" element={<CreateTask />} />
<Route path="edit/:id" element={<EditTask />} />
</Route>
</Routes>
);
}
</code>
<code>{/* previous imports */} import { TaskProvider } from './TaskContext'; // Import TaskProvider function App() { return ( <Routes> <Route path="/tasks" element={ // Wrap TaskProvider around List and its nested routes <TaskProvider> <List> <Outlet /> </List> </TaskProvider> } > <Route path="new" element={<CreateTask />} /> <Route path="edit/:id" element={<EditTask />} /> </Route> </Routes> ); } </code>
{/* previous imports */}
import { TaskProvider } from './TaskContext'; // Import TaskProvider

function App() {
  return (
    <Routes>
      <Route
        path="/tasks"
        element={
          // Wrap TaskProvider around List and its nested routes
          <TaskProvider>
            <List>
              <Outlet />
            </List>
          </TaskProvider>
        }
      >
        <Route path="new" element={<CreateTask />} />
        <Route path="edit/:id" element={<EditTask />} />
      </Route>
    </Routes>
  );
}

hope this helps!

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật