removed bootstrap dependencies

This commit is contained in:
Gene S
2016-01-26 17:27:29 -08:00
parent 876a7c1b1f
commit 3acd66e5b3
3 changed files with 254 additions and 7691 deletions
+186 -122
View File
@@ -4,18 +4,147 @@
margin: 0; margin: 0;
} }
/* Basic elements defined */
html, body { html, body {
height:100%; height:100%;
width:100%; width:100%;
font-family: 'Roboto', Arial, sans-serif; font-family: 'Roboto', Arial, sans-serif;
font-size: 10pt;
color: #666;
overflow: hidden; overflow: hidden;
background-color: silver; background-color: silver;
box-shadow: inset 0 10em #3949AB; /* the deep-plue stripe accross the top */
} }
a {
color: #2196f3;
text-decoration: none;
}
a:hover,
a:focus {
color: #0a6ebd;
text-decoration: underline;
}
a:focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
form {
font-size: 1.2rem;
}
/* Input fields and buttons */
button {
display: inline-block;
font-family: inherit;
font-size: 1.1rem;
height: 2.2em;
line-height: 2.2em;
font-weight: normal;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
border: 1px solid transparent;
white-space: nowrap;
padding-left: 2em;
padding-right: 2em;
border-radius: 2px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.25);
text-transform: uppercase;
}
button[disabled],
html input[disabled] {
cursor: default;
opacity: 0.65;
box-shadow: none;
}
button.round {
border-radius: 50%;
width: 3rem;
height: 3rem;
padding: 0;
line-height: 1;
outline: none; /* otherwise a square outline is shown */
box-shadow: 0 2px 0.5rem 0 rgba(0, 0, 0, 0.25);
z-index: 2;
}
button:focus, button:hover, button:active {
color: #fff;
}
button:active {
box-shadow: inset 0 3px 0.5rem rgba(0, 0, 0, 0.125);
}
button.blue {
background-color: #1e88e5; /* blue 600 */
color: white;
}
button.blue:focus, button.blue:hover, button.blue:active {
background-color: #1976d2;
}
textarea, input {
font-family: inherit;
background: transparent;
border: none;
border-bottom: 1px solid #ccc;
border-radius: 0.2em;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
width: 100%;
margin: 0.25em 0;
padding: 0 0.125em;
outline: none;
transition: all 0.2s ease-in-out;
box-sizing: border-box;
color: #666;
}
textarea:focus,input:focus {
border-bottom: 1px solid #2196f3;
box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25);
}
input {
height: 2.5em;
}
textarea {
resize: none;
}
/* Scrollbar styling for Chrome. Firefox does not support scrollbar styling. */
::-webkit-scrollbar {
width: 0.6rem;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.2);
background-color: rgba(255,255,255,0.1);
}
::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.3);
}
/* END of input and buttons */
form {
padding: 0.5em 1em;
line-height: 1.75;
font-size: 1.2rem;
}
/* END of basic elements */
#mountPoint { #mountPoint {
height: 100%; height: 100%;
padding-top: 1em; padding-top: 1em;
padding-bottom: 4em; padding-bottom: 3em;
} }
/* Top-level application layout style: sidepanel + main view */ /* Top-level application layout style: sidepanel + main view */
@@ -27,11 +156,13 @@ html, body {
overflow: hidden; overflow: hidden;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
border-radius: 0.25em;
box-shadow: 1px 2px 0.3em gray; box-shadow: 1px 2px 0.3em gray;
background-color: white; background-color: white;
} }
#sidepanel { #sidepanel {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 30em; width: 30em;
@@ -47,25 +178,35 @@ html, body {
/* END of top-level layout */ /* END of top-level layout */
/* Sidepanel styles */ /* Sidepanel styles */
.panel-title {
font-size: 1.4rem;
}
.dialog-buttons {
display: flex;
padding: 0.5em;
justify-content: flex-end;
}
#side-caption-panel { #side-caption-panel {
display: flex; display: flex;
align-items: center; align-items: center;
x-justify-content: space-between; x-justify-content: space-between;
flex: none; flex: none;
padding: 0.5em; padding: 0.5em;
height: 4.5em; height: 4em;
background-color: #039be5; /* light-blue 600 */ background-color: #2196f3; /* blue 500 */
color: #b3e5fc; /* light-blue 100 */ color: #bbdefb; /* light-blue 100 */
box-shadow: 0 1px 1px -1px gray; box-shadow: 0 1px 2px gray;
z-index: 1; /* otherwise the shadow becomes invisible */ z-index: 1; /* otherwise the shadow becomes invisible */
} }
#side-caption-panel a { #side-caption-panel a {
color: #b3e5fc; /* light-blue 100 */ color: #bbdefb; /* blue 100 */
padding-left: 0.5em; padding-left: 0.5em;
vertical-align: -1.5em; /* icons are bigger than the declared font size */ vertical-align: -1em; /* icons are bigger than the declared font size */
} }
#side-caption-panel :hover { #side-caption-panel :hover {
@@ -75,7 +216,6 @@ html, body {
#sidepanel-title { #sidepanel-title {
margin-right: auto; margin-right: auto;
margin-left: 0.5em; margin-left: 0.5em;
font-size: 2rem;
} }
#self-avatar { #self-avatar {
@@ -97,8 +237,8 @@ html, body {
/* List of contacts */ /* List of contacts */
#contacts-panel { #contacts-panel {
position: relative; overflow-y: auto;
overflow-y: scroll; margin-bottom: auto;
} }
#contact-box { #contact-box {
@@ -112,7 +252,14 @@ html, body {
overflow: hidden; overflow: hidden;
} }
#contact-box li:hover { #contact-box li:hover {
background-color: #fffde7; /* yellow 50 highlight */ background-color: #eceff1; /* blue-gray 50 highlight */
}
#contact-box li.selected {
background-color: #cfd8dc; /* blue-gray 100 highlight */
}
#contact-box .contact-title {
font-size: 1.05rem;
} }
/* Avatar and online-offline indicator */ /* Avatar and online-offline indicator */
@@ -132,9 +279,10 @@ html, body {
/* Online-offline indicator positioned inside avatar-box */ /* Online-offline indicator positioned inside avatar-box */
#contact-box .online, .offline { #contact-box .online, .offline {
font-size: 1.5em; font-size: 1.5em;
line-height: 1;
position: absolute; position: absolute;
right: 0; right: 0;
bottom: -0.5em; bottom: 0;
} }
#contact-box .online { #contact-box .online {
color: #66bb6a; color: #66bb6a;
@@ -159,24 +307,37 @@ html, body {
justify-content: space-between; justify-content: space-between;
flex: none; flex: none;
padding: 0.5em; padding: 0.5em;
height: 4.5em; height: 4em;
background-color: #b0bec5; background-color: #b0bec5;
box-shadow: 0 1px 1px -1px gray; box-shadow: 0 1px 2px gray;
z-index: 1; /* otherwise the shadow becomes invisible */ z-index: 1; /* otherwise the shadow becomes invisible */
} }
/*
#topic-title {
}
#topic-last-seen {
}
*/
/* Scrollable panel where the messages are shown */
#messages-panel { #messages-panel {
padding: 0.75em; padding: 0.75em;
overflow-y: scroll; overflow-y: scroll;
background-color: #eceff1; background-color: #eceff1;
height: 100%;
background-image: url("../img/ignasi_pattern_s.png"); background-image: url("../img/ignasi_pattern_s.png");
} }
/* Footer-panel with the input for the new message */ /* Footer-panel with the input for the new message */
#send-message-panel { #send-message-panel {
display: flex;
align-items: center;
flex: none; flex: none;
margin-top: auto;
padding: 0.5em; padding: 0.5em;
background-color: #eceff1;
box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
z-index: 2;
} }
/* A static panel shown when no topic is selected */ /* A static panel shown when no topic is selected */
@@ -189,53 +350,6 @@ html, body {
background-color: #e0f7fa; background-color: #e0f7fa;
} }
/* END of topic/mesages view */
.contactBox {
overflow: hidden;
}
#newTopicInput {
z-index: 3;
background-color: #FFF;
padding-bottom: 0.25em;
}
.contactList {
padding: 9.5em 0em 0.5em 0em;
margin-top: -9em;
height: 100vh;
overflow-y: scroll;
}
.contactList li {
padding: 0.25em;
}
.contactList li:hover {
background-color: #EEE;
}
.contact {
margin-top: 0;
}
.fullHeight {
height: 100%;
overflow: hidden;
}
.fullViewPort {
height: 99vh;
overflow-y: hidden;
}
.col-lg-12 {
padding: 0;
margin: 0;
}
/* Talk bubbles */ /* Talk bubbles */
.chat-box { .chat-box {
list-style: none; list-style: none;
@@ -373,65 +487,8 @@ html, body {
} }
/* END of talk bubbles */ /* END of talk bubbles */
/* END of topic/mesages view */
/* Input fields and buttons */
button {
display: inline-block;
margin: 0;
font-weight: normal;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
border: 1px solid transparent;
white-space: nowrap;
padding: 0.25em 1em;
font-size: 1.35rem;
border-radius: 2px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.25);
}
button[disabled],
html input[disabled] {
cursor: default;
opacity: 0.65;
box-shadow: none;
}
button.round {
border-radius: 50%;
width: 5rem;
height: 5rem;
line-height: 1;
outline: none; /* otherwise a square outline is shown */
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.25);
z-index: 2;
}
button.blue {
background-color: #2196f3; /* blue 500 */
color: white;
}
button:active {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}
input {
background: transparent;
border: none;
border-bottom: 1px solid #999;
width: 100%;
margin: 0;
padding: 0;
outline: none;
transition: border 0.2s ease-in-out, box-shadow: 0.2s ease-in-out;
}
input:focus {
border-bottom: 1px solid #2196f3;
box-shadow: inset 0 -1px 0 rgba(33, 150, 243, 0.25);
}
/* END of input and buttons */
/* Modal dialog */ /* Modal dialog */
.overlay { .overlay {
@@ -454,9 +511,15 @@ input:focus {
/* Badge with the number of unread messages */ /* Badge with the number of unread messages */
.badge { .badge {
display: inline-block;
font-size: 0.8rem;
font-weight: bold;
background-color: #0097A7; background-color: #0097A7;
color: white; color: white;
border-radius: 1em; border-radius: 1em;
padding: 0.25em;
min-width: 1.5em;
text-align: center;
} }
/* Color of buttons */ /* Color of buttons */
@@ -466,8 +529,9 @@ input:focus {
/* Material icons resized from the default 24pt */ /* Material icons resized from the default 24pt */
.material-icons { .material-icons {
font-size: 2rem; font-size: 1.4rem;
line-height: 1;
} }
.material-icons.small { .material-icons.small {
font-size: 1.5rem; font-size: 1rem;
} }
-7519
View File
File diff suppressed because it is too large Load Diff
+68 -50
View File
@@ -25,6 +25,11 @@
</head> </head>
</head> </head>
<body> <body>
<!--
<i class="material-icons">group</i>
<i class="material-icons" id="self-avatar">person</i>
-->
<div id="mountPoint"></div> <div id="mountPoint"></div>
<script type="text/babel"> <script type="text/babel">
// Host name and posrt to connect to // Host name and posrt to connect to
@@ -93,21 +98,20 @@
} }
return ( return (
<div> <div>
<form className="form-signin" onSubmit={this.handleSubmit}> <form id="login-form" onSubmit={this.handleSubmit}>
<label htmlFor="inputLogin" className="sr-only">Login</label> <input type="text" id="inputLogin"
<input type="text" id="inputLogin" className="form-control"
placeholder="Login (alice, bob, carol, dave, frank)" placeholder="Login (alice, bob, carol, dave, frank)"
value={this.state.login} value={this.state.login}
onChange={this.handleLoginChange} onChange={this.handleLoginChange}
required autofocus /> required autofocus />
<label htmlFor="inputPassword" className="sr-only">Password</label> <input type="password" id="inputPassword"
<input type="password" id="inputPassword" className="form-control"
placeholder="Password (alice123, bob123, ...)" placeholder="Password (alice123, bob123, ...)"
value={this.state.password} value={this.state.password}
onChange={this.handlePasswordChange} onChange={this.handlePasswordChange}
required /> required />
<br /> <div className="dialog-buttons">
<button className={submitClassList} type="submit">SIGN IN</button> <button className={submitClassList} type="submit">Sign in</button>
</div>
</form> </form>
</div> </div>
); );
@@ -159,24 +163,20 @@
} }
return ( return (
<div> <div>
<form onSubmit={this.handleSubmit} onCancel={this.props.onCancel}> <form id="signup-form" onSubmit={this.handleSubmit} onCancel={this.props.onCancel}>
<label htmlFor="signupLogin" className="sr-only">Login</label> <input type="text" placeholder="Login"
<input type="text" id="signupLogin" className="form-control" placeholder="Login"
value={this.state.login} onChange={this.handleLoginChange} required autofocus /> value={this.state.login} onChange={this.handleLoginChange} required autofocus />
<label htmlFor="signupPassword" className="sr-only">Password</label> <input type="password" placeholder="Password"
<input type="password" id="signupPassword" className="form-control" placeholder="Password"
value={this.state.password} onChange={this.handlePasswordChange} required /> value={this.state.password} onChange={this.handlePasswordChange} required />
<label htmlFor="signupPassword2" className="sr-only">Repeat password</label> <input type="password" placeholder="Repeat password"
<input type="password" id="signupPassword" className="form-control" placeholder="Repeat password"
value={this.state.password2} onChange={this.handlePassword2Change} required /> value={this.state.password2} onChange={this.handlePassword2Change} required />
<label htmlFor="signupPublic" className="sr-only">Account public data</label> <textarea rows="3" onChange={this.handlePublicChange}
<textarea id="signupPublic" className="form-control" rows="3" onChange={this.handlePublicChange}
value={this.state.public} placeholder="Public: text or valid JSON"></textarea> value={this.state.public} placeholder="Public: text or valid JSON"></textarea>
<label htmlFor="signupPrivate" className="sr-only">Account private data</label> <textarea rows="3" onChange={this.handlePrivateChange}
<textarea id="signupPrivate" className="form-control" rows="3" onChange={this.handlePrivateChange}
value={this.state.private} placeholder="Private: text or valid JSON"></textarea> value={this.state.private} placeholder="Private: text or valid JSON"></textarea>
<br /> <div className="dialog-buttons">
<button className={submitClassList} type="submit">SIGN UP</button> <button className={submitClassList} type="submit">Sign up</button>
</div>
</form> </form>
</div> </div>
); );
@@ -188,8 +188,8 @@
var SettingsView = React.createClass({ var SettingsView = React.createClass({
render: function() { render: function() {
return ( return (
<div className="container-fluid"> <div id="settings-form">
<h3>Not implemented</h3> <big>Not implemented</big>
</div> </div>
); );
} }
@@ -200,14 +200,15 @@
var SideNavbar = React.createClass({ var SideNavbar = React.createClass({
render: function() { render: function() {
var avatar = null; var avatar = null;
if (this.props.avatar) { if (this.props.avatar === true) {
var src = this.props.avatar === true ? "img/avatar.svg" : this.props.avatar; avatar = ( <i class="material-icons" id="self-avatar">person</i> );
avatar = <img id="self-avatar" alt="avatar" src={src} /> else if (this.props.avatar) {
avatar = ( <img id="self-avatar" alt="avatar" src={this.props.avatar} /> );
} }
return ( return (
<div id="side-caption-panel"> <div id="side-caption-panel">
{avatar} {avatar}
<div id="sidepanel-title">{this.props.title}</div> <div id="sidepanel-title" className="panel-title">{this.props.title}</div>
{this.props.state === 'login' ? {this.props.state === 'login' ?
<SideNavbar.MenuStart onSignUp={this.props.onSignUp} onSettings={this.props.onSettings} /> : <SideNavbar.MenuStart onSignUp={this.props.onSignUp} onSettings={this.props.onSettings} /> :
this.props.state === 'settings' ? this.props.state === 'settings' ?
@@ -225,9 +226,9 @@
render: function() { render: function() {
return ( return (
<div> <div>
<a href="#" onClick={this.props.onSignUp}><i className="material-icons medium">person_add</i></a> <a href="#" onClick={this.props.onSignUp}><i className="material-icons">person_add</i></a>
&nbsp; &nbsp;
<a href="#" onClick={this.props.onSettings}><i className="material-icons medium">settings</i></a> <a href="#" onClick={this.props.onSettings}><i className="material-icons">settings</i></a>
</div> </div>
); );
} }
@@ -236,7 +237,7 @@
SideNavbar.MenuCancel = React.createClass({ SideNavbar.MenuCancel = React.createClass({
render: function() { render: function() {
return ( return (
<a href="#" onClick={this.props.onCancel}><i className="material-icons medium">close</i></a> <a href="#" onClick={this.props.onCancel}><i className="material-icons">close</i></a>
); );
} }
}) })
@@ -244,7 +245,7 @@
SideNavbar.MenuSettings = React.createClass({ SideNavbar.MenuSettings = React.createClass({
render: function() { render: function() {
return ( return (
<a href="#" onClick={this.props.onSettings}><i className="material-icons medium">settings</i></a> <a href="#" onClick={this.props.onSettings}><i className="material-icons">settings</i></a>
); );
} }
}) })
@@ -359,14 +360,15 @@
if (title.length > 32) { if (title.length > 32) {
title = title.substring(0, 30) + "..."; title = title.substring(0, 30) + "...";
} }
var selected = (this.props.selected === this.props.topic) ? "selected" : null;
return ( return (
<li onClick={this.handleClick}> <li className={selected} onClick={this.handleClick}>
<div className="avatar-box"> <div className="avatar-box">
<img src="img/avatar.svg" className="avatar" alt="" /> <img src="img/avatar.svg" className="avatar" alt="" />
<span className={this.props.now ? "online" : "offline"}>&#x25cf;</span> <span className={this.props.now ? "online" : "offline"}>&#x25cf;</span>
</div> </div>
<div> <div>
<strong>{title}</strong> <Contact.Unread count={this.props.unread} /> <span className="contact-title">{title}</span> <Contact.Unread count={this.props.unread} />
<p>{this.props.comment}</p> <p>{this.props.comment}</p>
</div> </div>
</li> </li>
@@ -379,7 +381,7 @@
render: function() { render: function() {
var showUnreadBadge = null; var showUnreadBadge = null;
if (this.props.count > 0) { if (this.props.count > 0) {
var count = this.props.count > 99 ? "99+" : this.props.count; var count = this.props.count > 9 ? "9+" : this.props.count;
showUnreadBadge = <span className="badge">{count}</span>; showUnreadBadge = <span className="badge">{count}</span>;
} }
return ( return (
@@ -391,7 +393,10 @@
/* ContactsView holds all contacts-related stuff */ /* ContactsView holds all contacts-related stuff */
var ContactsView = React.createClass({ var ContactsView = React.createClass({
getInitialState: function() { getInitialState: function() {
return {subscriptions: []}; return {
subscriptions: [],
selected: null
};
}, },
componentWillMount: function() { componentWillMount: function() {
var me = Tinode.getMeTopic(); var me = Tinode.getMeTopic();
@@ -416,22 +421,18 @@
this.forceUpdate(); this.forceUpdate();
}, },
onSelectionChange: function(sel) { onSelectionChange: function(sel) {
this.setState({selected: sel});
this.props.onTopicSelected(sel); this.props.onTopicSelected(sel);
}, },
render: function() { render: function() {
var contactNodes = []; var contactNodes = [];
Tinode.getMeTopic().contacts(function(c) { Tinode.getMeTopic().contacts(function(c) {
var lastSeen = null;
if (c.seen) {
lastSeen = shortDateFormat(c.seen.when);
// TODO(gene): also handle user agent in c.seen.ua
}
var unread = c.seq; var unread = c.seq;
if (c.read > 0) { if (c.read > 0) {
unread -= c.read; unread -= c.read;
} }
contactNodes.push( contactNodes.push(
<Contact public={c.public} comment={c.private} unread={unread} now={c.online} when={lastSeen} <Contact public={c.public} comment={c.private} unread={unread} now={c.online} selected={this.state.selected}
onSelected={this.onSelectionChange} topic={c.topic} key={c.topic} /> onSelected={this.onSelectionChange} topic={c.topic} key={c.topic} />
); );
}, this); }, this);
@@ -543,12 +544,12 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
}, },
// Scroll last message into view on component update e.g. on message received. // Scroll last message into view on component update e.g. on message received.
componentDidUpdate: function() { componentDidUpdate: function() {
//var node = ReactDOM.findDOMNode(this); var node = this.refs.messagesPanel;
//if (node) { if (node) {
// node.scrollTop = node.scrollHeight; console.log("scrollToBottom: top=" + node.scrollTop + ", height="+ node.scrollHeight);
//} node.scrollTop = node.scrollHeight;
//var node = this.refs.messagesPanel; console.log("scrollToBottom: top=" + node.scrollTop + ", height="+ node.scrollHeight);
//node.scrollTop = node.scrollHeight; }
}, },
componentWillMount: function() { componentWillMount: function() {
this.propsChange(this.props); this.propsChange(this.props);
@@ -608,7 +609,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
var component = null; var component = null;
if (this.state.topic) { if (this.state.topic) {
var messageNodes = []; var messageNodes = [];
var me = Tinode.getCurrentUserID(); var myUid = Tinode.getCurrentUserID();
var topic = Tinode.getTopic(this.state.topic); var topic = Tinode.getTopic(this.state.topic);
var previousFrom = null; var previousFrom = null;
for (var i=0; i<this.state.messages.length; i++) { for (var i=0; i<this.state.messages.length; i++) {
@@ -626,7 +627,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
} }
previousFrom = msg.from; previousFrom = msg.from;
var isReply = !(msg.from === me); var isReply = !(msg.from === myUid);
var msgReceived = Received.UNDEF; var msgReceived = Received.UNDEF;
if (!isReply) { if (!isReply) {
if (topic.msgReadCount(msg.seq) > 1) { if (topic.msgReadCount(msg.seq) > 1) {
@@ -642,10 +643,27 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
userIndex={msg.$userIndex} sequence={sequence} received={msgReceived} key={msg.seq} /> userIndex={msg.$userIndex} sequence={sequence} received={msgReceived} key={msg.seq} />
); );
} }
var lastSeen = null;
/*
var cont = Tinode.getMeTopic().getContact(this.state.topic);
if (cont.getType() == "p2p") {
if (cont.online) {
lastSeen = "online now";
} else if (cont.seen) {
lastSeen = shortDateFormat(c.seen.when);
// TODO(gene): also handle user agent in c.seen.ua
}
}
*/
component = ( component = (
<div id="topic-view"> <div id="topic-view">
<div id="topic-caption-panel"> <div id="topic-caption-panel">
<h4>{this.state.title}</h4> <div>
<div id="topic-title" className="panel-title">{this.state.title}</div>
<div id="topic-last-seen">{lastSeen}</div>
</div>
<div>users online and menu buttons</div>
</div> </div>
<div id="messages-panel" ref="messagesPanel"> <div id="messages-panel" ref="messagesPanel">
<ul className="chat-box"> <ul className="chat-box">
@@ -713,7 +731,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
<a href="https://github.com/tinode/chat/"> <a href="https://github.com/tinode/chat/">
<div> <div>
<img id="logo" alt="logo" src="img/logo.svg" /> <img id="logo" alt="logo" src="img/logo.svg" />
<h4>Tinode Demo Chat</h4> <h2>Tinode Demo Chat</h2>
</div> </div>
</a> </a>
</div> </div>