This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git
The following commit(s) were added to refs/heads/master by this push:
new ca1778c1149 (feat) update ecosystem page & users swiper (#398)
ca1778c1149 is described below
commit ca1778c114965a3a247344162d89e54f495ca645
Author: hututu <[email protected]>
AuthorDate: Wed Jan 31 22:15:40 2024 +0800
(feat) update ecosystem page & users swiper (#398)
---
docusaurus.config.js | 10 +-
src/components/Icons/checked-icon.tsx | 4 +-
.../components/icons/data-loading.tsx | 42 +++++++++
.../ecomsystem-category/components/tab-item.tsx | 2 +-
.../ecomsystem-category/ecomsystem-category.tsx | 21 +++--
src/components/ecomsystem/ecomsystem.data.ts | 1 +
src/components/newsletter-swiper/index.tsx | 3 +-
src/pages/ecosystem/connectors/index.tsx | 9 +-
src/pages/ecosystem/data-loading/index.tsx | 44 +++++++++
src/pages/users/index.tsx | 85 +----------------
src/pages/users/user-swiper.tsx | 102 +++++++++++++++++++++
src/theme/BlogPostPage/index.tsx | 4 +-
static/images/ecomsystem/doris-stream-loader.png | Bin 0 -> 29207 bytes
13 files changed, 226 insertions(+), 101 deletions(-)
diff --git a/docusaurus.config.js b/docusaurus.config.js
index 869e405ac1b..a1ec4f62f86 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -253,11 +253,11 @@ const config = {
to: '/ecosystem/cluster-management',
position: 'left',
},
- // {
- // label: 'Community',
- // to: '/community/join-community',
- // position: 'left',
- // },
+ {
+ label: 'Community',
+ to: '/community/join-community',
+ position: 'left',
+ },
{
type: 'search',
position: 'right',
diff --git a/src/components/Icons/checked-icon.tsx
b/src/components/Icons/checked-icon.tsx
index 98429084d1f..ad4b0678189 100644
--- a/src/components/Icons/checked-icon.tsx
+++ b/src/components/Icons/checked-icon.tsx
@@ -1,8 +1,8 @@
import React from 'react';
-export function CheckedIcon() {
+export function CheckedIcon(props: any) {
return (
- <span>
+ <span {...props}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
viewBox="0 0 16 16" fill="none">
<rect width="16" height="16" fill="white" />
<path
diff --git
a/src/components/ecomsystem/ecomsystem-category/components/icons/data-loading.tsx
b/src/components/ecomsystem/ecomsystem-category/components/icons/data-loading.tsx
new file mode 100644
index 00000000000..6da18b5b18c
--- /dev/null
+++
b/src/components/ecomsystem/ecomsystem-category/components/icons/data-loading.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+
+export function DataLoadingIcon() {
+ return (
+ <svg xmlns="http://www.w3.org/2000/svg" width="54" height="54"
viewBox="0 0 54 54" fill="none">
+ <rect width="54" height="54" fill="white" />
+ <path
+ d="M32.7729 41.3351C32.7729 40.6693 33.3127 40.1296 33.9785
40.1296H41.2117C41.8775 40.1296 42.4172 40.6693 42.4172 41.3351C42.4172 42.0009
41.8775 42.5407 41.2117 42.5407H33.9785C33.3127 42.5407 32.7729 42.0009 32.7729
41.3351Z"
+ fill="#24195E"
+ />
+ <path
+ d="M7.05469 41.3351C7.05469 40.6693 7.59442 40.1296 8.26022
40.1296H15.4934C16.1592 40.1296 16.699 40.6693 16.699 41.3351C16.699 42.0009
16.1592 42.5407 15.4934 42.5407H8.26022C7.59442 42.5407 7.05469 42.0009 7.05469
41.3351Z"
+ fill="#24195E"
+ />
+ <path
+ d="M7.09766 11.025C7.09766 9.90662 8.00428 9 9.12266
9H41.5573C42.6757 9 43.5823 9.90662 43.5823 11.025V23.3819C43.5823 24.5003
42.6757 25.4069 41.5573 25.4069H9.12266C8.00428 25.4069 7.09766 24.5003 7.09766
23.3819V11.025Z"
+ fill="#444FD9"
+ />
+ <path
+ d="M7.09766 30.6539C7.09766 29.5355 8.00428 28.6289 9.12266
28.6289H37.0827C38.2011 28.6289 39.1077 29.5355 39.1077 30.6539V43.0107C39.1077
44.1291 38.2011 45.0357 37.0827 45.0357H9.12266C8.00428 45.0357 7.09766 44.1291
7.09766 43.0107V30.6539Z"
+ fill="#444FD9"
+ />
+ <rect x="13.3057" y="15.4534" width="10.5" height="3.5" rx="1.75"
fill="white" />
+ <rect x="13.3057" y="35.0823" width="10.5" height="3.5" rx="1.75"
fill="white" />
+ <path
+ d="M47.6322 37.8733C47.6322 43.121 43.3782 47.375 38.1306
47.375C32.8829 47.375 28.6289 43.121 28.6289 37.8733C28.6289 32.6257 32.8829
28.3717 38.1306 28.3717C43.3782 28.3717 47.6322 32.6257 47.6322 37.8733Z"
+ fill="white"
+ stroke="#444FD9"
+ stroke-width="3.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M42.5957 37.8733L34.4993 37.8733M34.4993 37.8733L38.3094
41.2071M34.4993 37.8733L38.3094 34.5395"
+ stroke="#444FD9"
+ stroke-width="2.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ );
+}
diff --git
a/src/components/ecomsystem/ecomsystem-category/components/tab-item.tsx
b/src/components/ecomsystem/ecomsystem-category/components/tab-item.tsx
index 771ec171e0c..a9005f99c09 100644
--- a/src/components/ecomsystem/ecomsystem-category/components/tab-item.tsx
+++ b/src/components/ecomsystem/ecomsystem-category/components/tab-item.tsx
@@ -14,7 +14,7 @@ interface PriceTabProps {
export function TabItem({ title, content, icon, active, setActive, url }:
PriceTabProps) {
return (
<Link
- to={url}
+ to={`/ecosystem/${url}`}
onClick={() => {
setActive();
}}
diff --git
a/src/components/ecomsystem/ecomsystem-category/ecomsystem-category.tsx
b/src/components/ecomsystem/ecomsystem-category/ecomsystem-category.tsx
index e49be97a497..86e394665e5 100644
--- a/src/components/ecomsystem/ecomsystem-category/ecomsystem-category.tsx
+++ b/src/components/ecomsystem/ecomsystem-category/ecomsystem-category.tsx
@@ -4,21 +4,22 @@ import { EcomsystemCategoryEnum } from '../ecomsystem.data';
import { ClusterManagementIcon } from
'./components/icons/cluster-management-icon';
import { ConnectorsIcon } from './components/icons/connectors-icon';
import { DistributionsAndPackagingIcon } from
'./components/icons/distributions-packaging-icon';
+import { DataLoadingIcon } from './components/icons/data-loading';
+
import { TabItem } from './components/tab-item';
export default function EcomsystemCategory() {
const location = useLocation();
const pathnames = location.pathname.split('/');
-
- const current = pathnames.length > 0 ? pathnames[pathnames.length - 1] :
'';
let currentActive: EcomsystemCategoryEnum =
EcomsystemCategoryEnum.ClusterManagement;
- if (current === EcomsystemCategoryEnum.ClusterManagement) {
+ if (pathnames.some(current => current ===
EcomsystemCategoryEnum.ClusterManagement)) {
currentActive = EcomsystemCategoryEnum.ClusterManagement;
- } else if (current === EcomsystemCategoryEnum.Connectors) {
+ } else if (pathnames.some(current => current ===
EcomsystemCategoryEnum.Connectors)) {
currentActive = EcomsystemCategoryEnum.Connectors;
} else {
- currentActive = EcomsystemCategoryEnum.DistributionsAndPackaging;
+ currentActive = EcomsystemCategoryEnum.DataLoading;
+ // currentActive = EcomsystemCategoryEnum.DistributionsAndPackaging;
}
const [active, setActive] = useState<EcomsystemCategoryEnum |
string>(currentActive);
@@ -42,13 +43,21 @@ export default function EcomsystemCategory() {
content="Integrate with Flink, Spark, dbt and more"
/>
<TabItem
+ url={EcomsystemCategoryEnum.DataLoading}
+ setActive={() => setActive(EcomsystemCategoryEnum.DataLoading)}
+ active={active === EcomsystemCategoryEnum.DataLoading}
+ icon={<DataLoadingIcon />}
+ title={'Data loading'}
+ content="Accelerate large-scale data loading"
+ />
+ {/* <TabItem
url={EcomsystemCategoryEnum.DistributionsAndPackaging}
setActive={() =>
setActive(EcomsystemCategoryEnum.DistributionsAndPackaging)}
active={active ===
EcomsystemCategoryEnum.DistributionsAndPackaging}
icon={<DistributionsAndPackagingIcon />}
title={'Distributions'}
content="Complement Apache Doris"
- />
+ /> */}
</div>
);
}
diff --git a/src/components/ecomsystem/ecomsystem.data.ts
b/src/components/ecomsystem/ecomsystem.data.ts
index 5e8895cc559..677c1d9e353 100644
--- a/src/components/ecomsystem/ecomsystem.data.ts
+++ b/src/components/ecomsystem/ecomsystem.data.ts
@@ -2,4 +2,5 @@ export enum EcomsystemCategoryEnum {
ClusterManagement = 'cluster-management',
Connectors = 'connectors',
DistributionsAndPackaging = 'distributions-and-packaging',
+ DataLoading = 'data-loading',
}
diff --git a/src/components/newsletter-swiper/index.tsx
b/src/components/newsletter-swiper/index.tsx
index ee1f9ea4b65..1f4d8e8611e 100644
--- a/src/components/newsletter-swiper/index.tsx
+++ b/src/components/newsletter-swiper/index.tsx
@@ -25,6 +25,7 @@ export function NewsLetterSwiper() {
useAnimationFrame(deltaTime => {
// Pass on a function to the setter of the state
// to make sure we always have the latest state
+ console.log(deltaTime);
setProgressCount(prevProgressCount => {
if (prevProgressCount >= 100) {
@@ -33,7 +34,7 @@ export function NewsLetterSwiper() {
}
if (deltaTime > 100) return prevProgressCount;
- return prevProgressCount + deltaTime * 0.01;
+ return prevProgressCount + deltaTime * 0.02;
});
}, stop);
diff --git a/src/pages/ecosystem/connectors/index.tsx
b/src/pages/ecosystem/connectors/index.tsx
index 3609904a0a4..c833cc223ef 100644
--- a/src/pages/ecosystem/connectors/index.tsx
+++ b/src/pages/ecosystem/connectors/index.tsx
@@ -3,6 +3,7 @@ import EcomsystemLayout from
'@site/src/components/ecomsystem/ecomsystem-layout/
import ExternalLink from '@site/src/components/external-link/external-link';
import CollapseBox from '@site/src/components/collapse-box/collapse-box';
import '../index.scss';
+import { ExternalLinkArrowIcon } from
'@site/src/components/Icons/external-link-arrow-icon';
export default function Connectors() {
return (
@@ -21,13 +22,14 @@ export default function Connectors() {
moreLink={
<>
<ExternalLink
-
href="https://github.com/apache/doris-flink-connector"
+
href="https://github.com/apache/doris-flink-connector/releases"
label="Download"
></ExternalLink>
<ExternalLink
href="https://doris.apache.org/docs/ecosystem/flink-doris-connector"
className="sub-btn"
label="Docs"
+ linkIcon={<ExternalLinkArrowIcon />}
></ExternalLink>
</>
}
@@ -43,13 +45,15 @@ export default function Connectors() {
moreLink={
<>
<ExternalLink
-
href="https://github.com/apache/doris-spark-connector"
+
href="https://github.com/apache/doris-spark-connector/releases"
label="Download"
></ExternalLink>
+
<ExternalLink
href="https://doris.apache.org/docs/ecosystem/spark-doris-connector"
className="sub-btn"
label="Docs"
+ linkIcon={<ExternalLinkArrowIcon />}
></ExternalLink>
</>
}
@@ -69,6 +73,7 @@ export default function Connectors() {
href="https://doris.apache.org/docs/ecosystem/dbt-doris-adapter"
className="sub-btn"
label="Docs"
+ linkIcon={<ExternalLinkArrowIcon />}
></ExternalLink>
</>
}
diff --git a/src/pages/ecosystem/data-loading/index.tsx
b/src/pages/ecosystem/data-loading/index.tsx
new file mode 100644
index 00000000000..8ac5490a71d
--- /dev/null
+++ b/src/pages/ecosystem/data-loading/index.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import EcomsystemLayout from
'@site/src/components/ecomsystem/ecomsystem-layout/ecomsystem-layout';
+import ExternalLink from '@site/src/components/external-link/external-link';
+import CollapseBox from '@site/src/components/collapse-box/collapse-box';
+import '../index.scss';
+import { ExternalLinkArrowIcon } from
'@site/src/components/Icons/external-link-arrow-icon';
+
+export default function DistributionsAndPackaging() {
+ return (
+ <EcomsystemLayout>
+ <div className="container mx-auto flex flex-col flex-wrap
items-center justify-center mb-[5.5rem] lg:flex-row">
+ <CollapseBox
+ title="Doris Streamloader"
+ description="A robust, high-performance and user-friendly
alternative to the traditional curl-based stream load."
+ characteristic={[
+ 'Split data files automatically and perform parallel
loading',
+ 'Support multiple files and directories load with one
shot',
+ 'Support path traversal when the target is a
directory',
+ 'Resume loading from previous failures and
cancellations',
+ 'Retry automatically when failure',
+ ]}
+ rightContent={
+ <img
src={require(`@site/static/images/ecomsystem/doris-stream-loader.png`).default}
alt="" />
+ }
+ moreLink={
+ <>
+ <ExternalLink
+
href="https://github.com/apache/doris-streamloader/releases"
+ label="GitHub"
+ ></ExternalLink>
+
+ <ExternalLink
+
href="https://doris.apache.org/docs/ecosystem/doris-streamloader"
+ className="sub-btn"
+ label="Docs"
+ linkIcon={<ExternalLinkArrowIcon />}
+ ></ExternalLink>
+ </>
+ }
+ />
+ </div>
+ </EcomsystemLayout>
+ );
+}
diff --git a/src/pages/users/index.tsx b/src/pages/users/index.tsx
index fb9effaea4c..08d8aba417f 100644
--- a/src/pages/users/index.tsx
+++ b/src/pages/users/index.tsx
@@ -5,9 +5,7 @@ import { translate } from '@docusaurus/Translate';
import './index.scss';
import userCasesEn from '@site/userCases/en_US.json';
import { Swiper, SwiperClass, SwiperSlide } from 'swiper/react';
-import 'swiper/css';
-import 'swiper/css/pagination';
-import 'swiper/css/navigation';
+
import { Pagination } from 'swiper';
import usePhone from '@site/src/hooks/use-phone';
import PageHeader from '@site/src/components/PageHeader';
@@ -17,6 +15,7 @@ import UserItem from './user-item';
import USERS from '../../constant/users.data.json';
import ReadMore from '@site/src/components/ReadMore';
import LinkWithArrow from '@site/src/components/link-arrow';
+import { UserSwiper } from './user-swiper';
const ALL_TEXT = 'All';
@@ -42,25 +41,8 @@ export default function Users(): JSX.Element {
setActive(ALL_TEXT);
}, []);
- const [swiperRef, setSwiperRef] = useState<SwiperClass>();
-
const theSlides = useMemo(() => ['slide one', 'slide two'], []);
- const handlePrevious = useCallback(() => {
- swiperRef?.slidePrev();
- }, [swiperRef]);
-
- const handleNext = useCallback(() => {
- swiperRef?.slideNext();
- }, [swiperRef]);
-
- const pagination = {
- clickable: true,
- renderBullet: function (index, className) {
- return '<span class="' + className + '"></span>';
- },
- };
-
function changeCategory(category: string) {
setActive(category);
let currentCategory = USER_STORIES_CATEGORIES.find(item => item ===
category);
@@ -76,67 +58,6 @@ export default function Users(): JSX.Element {
}
}
- function renderSwiper() {
- const modules = [Pagination];
- // if (!isPhone) {
- // modules.push(Navigation);
- // }
- return (
- <div style={{ position: 'relative' }}>
- {!isPhone && (
- <div
- onClick={handlePrevious}
- className="swiper-button-prev invisible
group-hover:visible"
- style={{ position: 'absolute', top: 'calc(50% -
2rem)', left: '-3rem', zIndex: 99 }}
- ></div>
- )}
-
- <Swiper
- pagination={pagination}
- spaceBetween={50}
- slidesPerView={1}
- navigation={false}
- modules={modules}
- loop={true}
- className="mySwiper"
- // style={{ minHeight: 480 }}
- onSlideChange={() => console.log('slide change')}
- onSwiper={setSwiperRef}
- >
- {USER_STORIES.map(userStory => {
- return (
- <SwiperSlide key={userStory.title}>
- <div className="users-wall-list row flex
flex-start pb-8 lg:pb-16">
- <div>
- <img
- className="users-wall-img
lg:w-[580px] lg:h-[248px]"
-
src={`${require(`@site/static/images/${userStory.image}`).default}`}
- alt="users-wall-img"
- />
- </div>
- <div className="w-[35.75rem] ml-4 lg:ml-12
flex flex-col py-4">
- <h3 className="leading-[38px]
text-2xl">{userStory.title}</h3>
- <p className="my-6 text-base">
- <strong
className="font-normal">{userStory.author.name}</strong>
- <span className="ml-6
text-[#4C576C]">{userStory.author.title}</span>
- </p>
- <ReadMore to={userStory.to}
className="text-primary" />
- </div>
- </div>
- </SwiperSlide>
- );
- })}
- </Swiper>
- {!isPhone && (
- <div
- onClick={handleNext}
- className="swiper-button-next invisible
group-hover:visible"
- style={{ position: 'absolute', top: 'calc(50% -
2rem)', right: '-3rem', zIndex: 99 }}
- ></div>
- )}
- </div>
- );
- }
return (
<Layout
title={translate({ id: 'users.title', message: 'User Stories' })}
@@ -162,7 +83,7 @@ export default function Users(): JSX.Element {
/>
<section className="group">
- <div className="users-wall container lg:pt-[88px]
">{renderSwiper()}</div>
+ <div className="users-wall container lg:pt-[88px]
">{<UserSwiper />}</div>
</section>
<section className="lg:pt-[5.5rem] container pb-[88px]">
<div className="blog-list-wrap row mt-28 lg:mt-0">
diff --git a/src/pages/users/user-swiper.tsx b/src/pages/users/user-swiper.tsx
new file mode 100644
index 00000000000..291b6bd9a4a
--- /dev/null
+++ b/src/pages/users/user-swiper.tsx
@@ -0,0 +1,102 @@
+import usePhone from '@site/src/hooks/use-phone';
+import { Pagination } from 'swiper';
+import React, { useCallback, useState } from 'react';
+import { Swiper, SwiperClass, SwiperSlide } from 'swiper/react';
+import 'swiper/css';
+import 'swiper/css/pagination';
+import 'swiper/css/navigation';
+import { USER_STORIES, USER_STORIES_CATEGORIES } from
'@site/src/constant/user.data';
+import ReadMore from '@site/src/components/ReadMore';
+import { useAnimationFrame } from '../../hooks/use-animation-frame';
+
+export function UserSwiper() {
+ const { isPhone } = usePhone();
+ const [progressCount, setProgressCount] = useState<number>(0);
+ const [stop, setStop] = useState<boolean>(false);
+
+ const [swiperRef, setSwiperRef] = useState<SwiperClass>();
+
+ const handlePrevious = useCallback(() => {
+ swiperRef?.slidePrev();
+ }, [swiperRef]);
+
+ const handleNext = useCallback(() => {
+ swiperRef?.slideNext();
+ }, [swiperRef]);
+
+ const pagination = {
+ clickable: true,
+ renderBullet: function (index, className) {
+ return '<span class="' + className + '"></span>';
+ },
+ };
+
+ useAnimationFrame(deltaTime => {
+ // Pass on a function to the setter of the state
+ // to make sure we always have the latest state
+ console.log(deltaTime);
+ setProgressCount(prevProgressCount => {
+ if (prevProgressCount >= 100) {
+ handleNext();
+ return 0;
+ }
+ if (deltaTime > 100) return prevProgressCount;
+
+ return prevProgressCount + deltaTime * 0.02;
+ });
+ }, stop);
+
+ return (
+ <div style={{ position: 'relative' }} onMouseMove={() =>
setStop(true)} onMouseLeave={() => setStop(false)}>
+ {!isPhone && (
+ <div
+ onClick={handlePrevious}
+ className="swiper-button-prev invisible
group-hover:visible"
+ style={{ position: 'absolute', top: 'calc(50% - 2rem)',
left: '-3rem', zIndex: 99 }}
+ ></div>
+ )}
+
+ <Swiper
+ pagination={pagination}
+ spaceBetween={50}
+ slidesPerView={1}
+ navigation={false}
+ modules={[Pagination]}
+ loop={true}
+ onSlideChange={() => setProgressCount(0)}
+ onSwiper={setSwiperRef}
+ >
+ {USER_STORIES.map(userStory => {
+ return (
+ <SwiperSlide key={userStory.title}>
+ <div className="users-wall-list row flex
flex-start pb-8 lg:pb-16">
+ <div>
+ <img
+ className="users-wall-img lg:w-[580px]
lg:h-[248px]"
+
src={`${require(`@site/static/images/${userStory.image}`).default}`}
+ alt="users-wall-img"
+ />
+ </div>
+ <div className="w-[35.75rem] ml-4 lg:ml-12
flex flex-col py-4">
+ <h3 className="leading-[38px]
text-2xl">{userStory.title}</h3>
+ <p className="my-6 text-base">
+ <strong
className="font-normal">{userStory.author.name}</strong>
+ <span className="ml-6
text-[#4C576C]">{userStory.author.title}</span>
+ </p>
+ <ReadMore to={userStory.to}
className="text-primary" />
+ </div>
+ </div>
+ </SwiperSlide>
+ );
+ })}
+ </Swiper>
+ {!isPhone && (
+ <div
+ onClick={handleNext}
+ className="swiper-button-next invisible
group-hover:visible"
+ style={{ position: 'absolute', top: 'calc(50% - 2rem)',
right: '-3rem', zIndex: 99 }}
+ ></div>
+ )}
+ </div>
+ );
+}
diff --git a/src/theme/BlogPostPage/index.tsx b/src/theme/BlogPostPage/index.tsx
index 0a75fb8a1ae..b6351a8c52b 100644
--- a/src/theme/BlogPostPage/index.tsx
+++ b/src/theme/BlogPostPage/index.tsx
@@ -33,7 +33,7 @@ function BlogPostPageContent(props: { sidebar: BlogSidebar;
children: ReactNode
}
>
<BlogPostItem>{children}</BlogPostItem>
- <div className="scrollbar-none w-[100%] mt-6 custom-scrollbar
m-auto flex gap-3 overflow-auto text-[#4C576C] lg:mt-12 lg:gap-4 pl-4">
+ {/* <div className="scrollbar-none w-[100%] mt-6 custom-scrollbar
m-auto flex gap-3 overflow-auto text-[#4C576C] lg:mt-12 lg:gap-4 pl-4">
{tags.map((item: any, index) => (
<Link className="py-px"
to={`/blog?currentPage=1¤tCategory=${item.label}`} key={index}>
<span
@@ -43,7 +43,7 @@ function BlogPostPageContent(props: { sidebar: BlogSidebar;
children: ReactNode
</span>
</Link>
))}
- </div>
+ </div> */}
{/* {(nextItem || prevItem) && <BlogPostPaginator
nextItem={nextItem} prevItem={prevItem} />} */}
<RecentBlogs />
</BlogLayout>
diff --git a/static/images/ecomsystem/doris-stream-loader.png
b/static/images/ecomsystem/doris-stream-loader.png
new file mode 100644
index 00000000000..4951b807026
Binary files /dev/null and b/static/images/ecomsystem/doris-stream-loader.png
differ
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]